美文网首页
EduSoho框架DAO层缓存机制

EduSoho框架DAO层缓存机制

作者: 奈何蜀黍 | 来源:发表于2019-03-22 17:28 被阅读0次

EduSoho框架从2013年发布首个开源版本以来,收到了几万条用户真实需求及优化建议。Edusoho主产品历时5年多的不间断的迭代,发布了380多个版本,目前已得到了国内多家互联网巨头企业及大型培训机构的认可。

2013年花了三天三夜重构出首个版本的大神的github主页,欢迎大家前来围观

EduSoho框架的灵活性及可扩展性使其成为了一个有生命力的技术框架。下文主要介绍了EduSoho框架中较为底层的DAO层缓存机制。

1. BizFramework

说到DAO层,熟悉MVC框架的朋友肯定会想到MVC中的数据持久层。没错,这里说的DAO层确实是MVC框架的一部分。EduSoho使用的MVC框架是BizFramework。

BizFramework 简介

BizFramework 是在EduSoho框架不断迭代过程中结合实际应用场景抽离出来的通用业务层框架,简称Biz,后续文中统一使用Biz。

BizFramework 特点

简单、易用

开发人员可以采用经典的 Service/Dao 模式来编写业务逻辑

支持 Symfony、Laravel、Silex、Lumen、Phalcon 等主流PHP框架

BizFramework 使用方式

使用Biz框架写Dao层代码

和常规的 MVC 框架一致,首先定义 DAO 接口。Biz框架中定义了基础 DAO 接口 GeneralDaoInterface 以及对应的实现:GeneralDaoImpl。GeneralDaoInterface 接口如下,定义了常用的增删改查的数据操作方法。

<?php

namespace Codeages\Biz\Framework\Dao;

interface GeneralDaoInterface

{

public function create($fields);

public function update($id, array $fields);

public function delete($id);

public function get($id);

public function search($conditions, $orderBy, $start, $limit);

public function count($conditions);

public function wave(array $ids, array $diffs);

}

我们在声明自己的 DAO 接口时,可以继承 GeneralDaoInterface。以课程对应的 DAO 接口为例, 直接继承自 GeneralDaoInterface 的默认接口,这些接口都是常规的通用接口,包括增删改查,减少了开发者的重复工作。

#课程对应的Dao接口

<?php

namespace Biz\Course\Dao;

use Codeages\Biz\Framework\Dao\GeneralDaoInterface;

interface CourseDao extends GeneralDaoInterface

{

public function getByName($name);

}

在编写 DAO 实现类时,我们继承 GeneralDaoImpl 。例如课程对应的 DAO 实现类,继承 GeneralDaoImpl 后会默认实现继承自GeneralDaoInterface 的接口,所以开发者只要按约定来开发代码,也不需要额外再去实现通用的接口方法。

<?php

namespace Biz\Course\Dao\Impl;

use Codeages\Biz\Framework\Dao\GeneralDaoImpl;

use Biz\Course\Dao\CourseDao;

class CourseDaoImpl extends GeneralDaoImpl implements CourseDao

{

protected $table = 'course';

public function getByName($name)

{

return $this->getByField('name', $name);

}

public function declares()

{

return array(

'timestamps' => array('created_time', 'update_time'),

'serializes' => array(),

'orderbys' => array(),

'conditions' => array(

'type = :type',

),

);

}

}

Service层调用Dao的方法的写法

为了让开发者可以快速上手Biz框架,Biz框架基本上保持了和常规MVC框架一致的代码书写形式,例如Spring MVC。

<?php

namespace Biz\Course\Service\Impl;

class CourseServiceImpl extends BaseService implements CourseService

{

public function getCourseByName($name)

{

$course = $this->getCourseDao()->getByName($name);

return $course;

}

protected function getCourseDao()

{

return $this->biz->dao('Course:CourseDao');

}

}

2. DAO 代理

基于上述内容,我们得知使用Biz框架写 DAO 层和常规的MVC框架开发形式基本一致。那么 DAO 层的缓存机制到底是怎么工作的呢?

Servcie中获取 DAO 实例的代码

protected function getCourseDao()

{

return $this->biz->dao('Course:CourseDao');

}

可以理解为从biz容器中获取 CourseDaoImpl 实例,CourseDaoImpl 实例是按需初始化的,也就是说会在第一次调用的时候进行实例化,然后后续调用返回此单例。

biz实例化Dao的代码

可以发现实际上返回的是 Dao 的代理类 DaoProxy,而非 CourseDaoImpl。

<?php

namespace Codeages\Biz\Framework\Context;

...

class Biz extends Container

{

...

public function __construct(array $values = array())

....

$biz['autoload.object_maker.dao'] = function ($biz) {

return function ($namespace, $name) use ($biz) {

$class = "{$namespace}\\Dao\\Impl\\{$name}Impl";

#返回Dao代理类——DaoProxy

return new DaoProxy($biz, new $class($biz), $biz['dao.metadata_reader'], $biz['dao.serializer'], $biz['dao.cache.array_storage']);

};

};

...

}

...

}

DaoProxy

DaoProxy 中包含了 Dao 层缓存工作的核心代码,代理了大部分的查询方法,并且根据对应的缓存策略,控制着真实的 Dao 调用或者是缓存的生成及失效。

DaoProxy 优势

开发者甚至可以无感知 Dao 代理类的存在即可启用 Dao 层的缓存

默认缓存策略能适用大部分的应用场景

提升性能

<?php

namespace Codeages\Biz\Framework\Dao;

use Codeages\Biz\Framework\Dao\Annotation\MetadataReader;

class DaoProxy

{

...

protected function getProxyMethod($method)

{

foreach (array('get', 'find', 'search', 'count', 'create', 'batchCreate', 'batchUpdate', 'batchDelete', 'update', 'wave', 'delete') as $prefix) {

if (0 === strpos($method, $prefix)) {

return $prefix;

}

}

return null;

}

/**

* @return CacheStrategy|null

*/

private function buildCacheStrategy()

{

if (!empty($this->cacheStrategy)) {

return $this->cacheStrategy;

}

if (empty($this->container['dao.cache.enabled'])) {

return null;

}

if (!empty($this->container['dao.cache.annotation'])) {

$strategy = $this->getCacheStrategyFromAnnotation($this->dao);

if ($strategy) {

return $strategy;

}

}

$declares = $this->dao->declares();

// 未指定 cache 策略,则使用默认策略

if (!isset($declares['cache'])) {

return $this->container['dao.cache.strategy.default'];

}

// 针对某个 Dao 关闭 Cache

if (false === $declares['cache']) {

return null;

}

// 针对某个 Dao 指定 Cache 策略

$strategyServiceId = 'dao.cache.strategy.'.strtolower($declares['cache']);

if (!isset($this->container[$strategyServiceId])) {

throw new DaoException("Dao %s cache strategy is not defined, please define first in biz container use %s service id.", get_class($this->dao), $strategyServiceId);

}

return $this->container[$strategyServiceId];

}

...

}

从以上代码可以得出如下结论:

DaoProxy 默认代理了get, find, search, count, create, batchCreate, batchUpdate, batchDelete, update, wave, delete开头的方法

每个 Dao 都可以指定 Cache 策略,不指定则使用默认策略

3. 默认缓存策略

BizFramework 中的默认缓存策略是表级缓存,表级缓存意味着如果表有写操作则所有该表相关的缓存都会失效。

优点

易用

适用面广,适用于大部分查询多于写操作的场景

<?php

namespace Codeages\Biz\Framework\Dao\CacheStrategy;

class TableStrategy implements CacheStrategy

{

...

public function beforeQuery(GeneralDaoInterface $dao, $method, $arguments)

{

$key = $this->key($dao, $method, $arguments);

return $this->redis->get($key);

}

public function afterQuery(GeneralDaoInterface $dao, $method, $arguments, $data)

{

$key = $this->key($dao, $method, $arguments);

return $this->redis->set($key, $data, self::LIFE_TIME);

}

public function afterCreate(GeneralDaoInterface $dao, $method, $arguments, $row)

{

$this->upTableVersion($dao);

}

public function afterUpdate(GeneralDaoInterface $dao, $method, $arguments, $row)

{

$this->upTableVersion($dao);

}

private function getTableVersion($dao)

{

$key = sprintf('dao:%s:v', $dao->table());

if (isset($this->storage[$key])) {

return $this->storage[$key];

}

$version = $this->redis->get($key);

if (false === $version) {

$version = $this->redis->incr($key);

}

$this->storage[$key] = $version;

return $version;

}

private function upTableVersion($dao)

{

$key = sprintf('dao:%s:v', $dao->table());

$version = $this->storage[$key] = $this->redis->incr($key);

return $version;

}

private function key(GeneralDaoInterface $dao, $method, $arguments)

{

$version = $this->getTableVersion($dao);

$key = sprintf('dao:%s:v:%s:%s:%s', $dao->table(), $version, $method, json_encode($arguments));

return $key;

}

...

}

DaoImpl 中被代理的查询方法执行前会调用 beforeQuery 查询缓存。

如果缓存命中,直接返回缓存;

如果缓存不命中,执行真实的方法逻辑查询数据,方法执行完后调用 afterQuery 存储缓存。

每个表在缓存中都对应维护了一个版本号,如果有写操作则会调用 upTableVersion 方法更新版本号。可以看到所有该表相关的缓存 key 都会携带版本号信息,调用 key 方法获取最新 key,这样基于老版本号生成的 key 自然就会失效等待异步回收。

4. 缓存策略扩展

在某些特定场景下,默认缓存策略并不能适用,好在 Biz 框架允许我们扩展缓存策略。之前项目中遇到过的场景比较典型,在这里和大家分享一二。

场景1: 支持连表查询

Biz 框架推荐所有的查询都是单表查询(方便做分表分库),默认缓存策略也不支持连表查询。但是在实际项目中可能需要用到连表查询。

实现连表查询的方案

借鉴默认策略中缓存版本号控制的方式,我们稍加修改就可以实现连表查询的缓存策略。思路很简单,把 join 的多个表的版本号组合成新的版本号作为 join 查询缓存的版本号。这样 join 表中任意一个表的版本号更新都会使 join 查询缓存失效。这样就可以确保 join 查询缓存中的数据的实时性了。

<?php

namespace CustomBundle\Common\BizFramework\CacheStrategy;

/**

* 多表联查表级别缓存策略

*/

class TableJoinQueryStrategy implements CacheStrategy

{

public function beforeQuery(GeneralDaoInterface $dao, $method, $arguments)

{

$key = $this->key($dao, $method, $arguments);

return $this->redis->get($key);

}

public function afterQuery(GeneralDaoInterface $dao, $method, $arguments, $data)

{

$key = $this->key($dao, $method, $arguments);

return $this->redis->set($key, $data, self::LIFE_TIME);

}

private function key(GeneralDaoInterface $dao, $method, $arguments)

{

$version = $this->getTableVersion($dao);

$daoName = $this->getDaoName($dao);

$key = sprintf('dao:%s:v:%s:%s:%s', $daoName, $version, $method, json_encode($arguments));

return $key;

}

private function getDaoName($dao)

{

$tables = $dao->getJoinTables();

return implode('-', $tables);

}

private function getTableVersion($dao)

{

$tables = $dao->getJoinTables();

$version = '';

foreach ($tables as $table) {

$version = $version.$this->getSingleTableVersion($table).'-';

}

return $version;

}

private function getSingleTableVersion($tableName)

{

$key = sprintf('dao:%s:v', $tableName);

if (isset($this->storage[$key])) {

return $this->storage[$key];

}

$version = $this->redis->get($key);

if (false === $version) {

$version = $this->redis->incr($key);

}

$this->storage[$key] = $version;

return $version;

}

}

场景2:不需要较高实时性的数据,比如说数据报表

默认缓存策略和场景1的策略为了保证缓存数据的实时性,牺牲了一部分缓存的命中率。如果不需要很高的数据实时性,建议采用另一种更为简单的缓存策略 —— 有效期失效缓存策略。

有效期失效的缓存策略

即给缓存设置一个有效期,在有效期内不考虑数据的实时性直接返回缓存数据。适用于数据统计、数据报表等对实时性要求不高的场景。具体代码如下。

<?php

namespace CustomBundle\Common\BizFramework\CacheStrategy;

/**

* 固定失效周期缓存策略

*/

class ExpireIntervalStrategy implements CacheStrategy

{

#固定失效时间为1小时

const LIFE_TIME = 3600;

public function beforeQuery(GeneralDaoInterface $dao, $method, $arguments)

{

$key = $this->key($dao, $method, $arguments);

return $this->redis->get($key);

}

public function afterQuery(GeneralDaoInterface $dao, $method, $arguments, $data)

{

$key = $this->key($dao, $method, $arguments);

return $this->redis->set($key, $data, self::LIFE_TIME);

}

private function key(GeneralDaoInterface $dao, $method, $arguments)

{

$daoName = $this->getDaoName($dao);

$key = sprintf('dao:%s:%s:%s', $daoName, $method, json_encode($arguments));

return $key;

}

private function getDaoName($dao)

{

return $dao->getDaoName();

}

}

5. 踩坑案例

在此和大家分享一个使用默认缓存策略遇到过的坑点。

坑的场景

用户表中有个用于记录用户最后登入时间的字段,用户每次登入都会更新该字段。当大量用户并发登入时,该字段的内容会频繁更新,缓存版本号不停往上涨,导致了短时间内大量失效缓存占用大量内存未能及时回收的问题。

解决方案

将该字段的更新操作单独拎出来写成方法,同时要注意该方法命名不能符合 DaoProxy 的代理前缀。这样就意味着该字段的更新操作不会导致对应的缓存版本号更新。由于该字段不需要很高的实时性,所以可以等待缓存默认失效时间到达后再刷新数据。这样的话就不需要额外写缓存刷新逻辑,提高了工作效率。

6. Q&A

如果你也遇到过坑欢迎留言探讨。

更多关于BizFramework的使用说明, 感兴趣的同学可以看下官方开发文档。

官方开发文档地址

EduSoho官网 https://www.edusoho.com/

EduSoho开源地址 https://github.com/edusoho/edusoho

---------------------

作者:Codeages

来源:CSDN

原文:https://blog.csdn.net/weixin_43757847/article/details/88186656

相关文章

  • EduSoho框架DAO层缓存机制

    EduSoho框架从2013年发布首个开源版本以来,收到了几万条用户真实需求及优化建议。Edusoho主产品历时5...

  • Hibernate笔记

    JavaEE的三层结构 web层:struts2框架 service层:spring框架 dao层:hiberna...

  • Hibernate学习(环境搭建)

    JavaEE三层结构 web层Struts2框架 service层Spring框架 dao层hibernate M...

  • SpringMVC--初入SpringMVC

    后台人员主要处理数据库交互以及请求交互,即DAO层和Controller层,其中MyBatis是对应DAO层的框架...

  • iOS-网络-AFNetworking添加缓存层

    为什么要添加一层缓存层? 系统有默认的缓存机制,用自己的缓存机制有绝对把控权 缓存的时效(A到B页面,B页面返回,...

  • b2db的使用操作

    b2db是一个dao层的框架,现在网络上有好多golang Dao的框架,比如beegoo.rm、beedb;在...

  • Android缓存机制——一般存储实现

    一、Android缓存机制 Android缓存分为内存缓存和文件缓存(磁盘缓存)。在早期,各大图片缓存框架流行之前...

  • Hibernate框架

    Hibernate:基于持久层的框架(数据访问层使用) Service业务逻辑层 Dao 数据访问层 ORM(Ob...

  • Service层Mock测试 2018-05-15

    (关键词:逻辑层(service)和数据访问层(Dao) Unitils、Spring测试框架、UnitilsJU...

  • 框架整合

    框架整合 Dao层 mybatis整合spring,通过spring管理SqlSessionFactory、map...

网友评论

      本文标题:EduSoho框架DAO层缓存机制

      本文链接:https://www.haomeiwen.com/subject/kxbrvqtx.html