EduSoho框架DAO层缓存机制

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

namespaceCodeages\Biz\Framework\Dao;

interfaceGeneralDaoInterface

{

publicfunctioncreate($fields);

publicfunctionupdate($id,array$fields);

publicfunctiondelete($id);

publicfunctionget($id);

publicfunctionsearch($conditions,$orderBy,$start,$limit);

publicfunctioncount($conditions);

publicfunctionwave(array$ids,array$diffs);

}

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

返回Dao代理类——DaoProxy

returnnewDaoProxy($biz,new$class($biz),$biz['_reader'],$biz[''],$biz['_storage']);

};

};

}

}

DaoProxy

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

DaoProxy优势

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

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

提升性能

?php

namespaceCodeages\Biz\Framework\Dao;

useCodeages\Biz\Framework\Dao\Annotation\MetadataReader;

classDaoProxy

{

protectedfunctiongetProxyMethod($method)

{

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

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

return$prefix;

}

}

returnnull;

}

/**

*@returnCacheStrategy|null

*/

privatefunctionbuildCacheStrategy()

{

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

return$this-cacheStrategy;

}

if(empty($this-container[''])){

returnnull;

}

if(!empty($this-container[''])){

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

if($strategy){

return$strategy;

}

}

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

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

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

return$this-container[''];

}

//针对某个Dao关闭Cache

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

returnnull;

}

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

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

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

thrownewDaoException("Dao%scachestrategyisnotdefined,pleasedefinefirstinbizcontaineruse%sserviceid.",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

namespaceCodeages\Biz\Framework\Dao\CacheStrategy;

classTableStrategyimplementsCacheStrategy

{

publicfunctionbeforeQuery(GeneralDaoInterface$dao,$method,$arguments)

{

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

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

}

publicfunctionafterQuery(GeneralDaoInterface$dao,$method,$arguments,$data)

{

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

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

}

publicfunctionafterCreate(GeneralDaoInterface$dao,$method,$arguments,$row)

{

$this-upTableVersion($dao);

}

publicfunctionafterUpdate(GeneralDaoInterface$dao,$method,$arguments,$row)

{

$this-upTableVersion($dao);

}

privatefunctiongetTableVersion($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;

}

privatefunctionupTableVersion($dao)

{

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

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

return$version;

}

privatefunctionkey(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

namespaceCustomBundle\Common\BizFramework\CacheStrategy;

/**

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

*/

classTableJoinQueryStrategyimplementsCacheStrategy

{

publicfunctionbeforeQuery(GeneralDaoInterface$dao,$method,$arguments)

{

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

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

}

publicfunctionafterQuery(GeneralDaoInterface$dao,$method,$arguments,$data)

{

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

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

}

privatefunctionkey(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;

}

privatefunctiongetDaoName($dao)

{

$tables=$dao-getJoinTables();

returnimplode('-',$tables);

}

privatefunctiongetTableVersion($dao)

{

$tables=$dao-getJoinTables();

$version='';

foreach($tablesas$table){

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

}

return$version;

}

privatefunctiongetSingleTableVersion($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

namespaceCustomBundle\Common\BizFramework\CacheStrategy;

/**

*固定失效周期缓存策略

*/

classExpireIntervalStrategyimplementsCacheStrategy

{

#固定失效时间为1小时

constLIFE_TIME=3600;

publicfunctionbeforeQuery(GeneralDaoInterface$dao,$method,$arguments)

{

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

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

}

publicfunctionafterQuery(GeneralDaoInterface$dao,$method,$arguments,$data)

{

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

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

}

privatefunctionkey(GeneralDaoInterface$dao,$method,$arguments)

{

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

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

return$key;

}

privatefunctiongetDaoName($dao)

{

return$dao-getDaoName();

}

}

5.踩坑案例

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

坑的场景

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


解决方案

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

6.QA

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

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

官方开发文档地址

EduSoho官网

EduSoho开源地址

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

作者:Codeages

原文:

版权声明:本文为博主原创文章,转载请附上博文链接!

版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。

相关推荐