基于PHP的一种Cache回调与自动触发技术

背景

在PHP中使用Memcache或者Redis时,我们一般都会对Memcache和Redis封装一下,单独完成写一个Cache类,作为Memcache活着Redis的代理,且一般为单例模式。在业务代码中,使用Cache类时,操作的基本的示例代码如下

// cache 的 key
$key = ‘this is key‘;
$expire = 60;// 超时时间

// cache 的实例
$cache = Wk_Cache::instance();
$data = $cache->fetch($key);

// 判断data
if(empty($data)){
    // 如果为空,调用db方法
    $db = new Wk_DB();
    $data = $db->getXXX();
    $cache->store($key, $data, $expire);
}
// 处理$data相关数据
return $data;

基本流程为
第一步,先组装查询key,到Cache查询Value,如果存在,继续处理,进入第三步;如果不存在,进入第二步
第二步,根据请求,到DB中,查询相关数据,如果数据存在,把数据放到Cache中
第三步,处理cache或者db中返回的数据

问题

上述流程基本上会出现在每次调用Cache的部分,先cache查询,没有的话调用DB或者第三方接口,获取数据,再次存入Cache,继续数据处理。多次调用,既是一种问题,应该把这种查询方式封装到更底层的方法内。而不是每次重复这样的逻辑,除了封装的问题外,还有其他问题,我们统一列举下

第一:从设计角度来说 重复代码,需要更底层逻辑封装。
第二:key的组装,麻烦繁琐,实际情况,可能会把各种参数组装进去,维护的时候,不敢修改。
第三:设置的expire超时时间,会分散在各处逻辑代码中,最终很难统计Cache缓存时间的情况。
第四:由于要把cache->store方法放到调用db之后执行,如果db后,还有其他逻辑处理,有可能会忘掉把数据放入cache,导致数据。
第五:在高并发系统中,cache失效那一刻,会有大量请求直接穿透到后方,导致DB或者第三方接口压力陡升,响应变慢,进一步影响系统稳定性,这一现象为“Dogpile”。

以上问题中,最简单的为2,3种,对于expire超时时间分散的问题,我们可以通过统一配置文件来解决,比如我们可以创建这样的一个配置文件。

“test"=>array( // namespace,方便分组
             "keys"=> array(
                 “good”=>array(		// 定义的key,此key非最终入cache的key,入key需要和params组装成唯一的key
                     "timeout"=>600,	// 此处定义超时时间
                     "params"=>array("epid"=>1,"num"=>1),	// 通过这种方法,描述需要传递参数,用于组装最终入cache的key
                     "desc"=>"描述"
                     ),
                "top_test"=>array(	// 定义的key,此key非最终入cache的key,入key需要和params组装成唯一的key
                     "timeout"=>60,	// 此处定义超时时间
                     "ttl"=>10,	// 自动触发时间
                     "params"=>array(‘site_id‘=>1,‘boutique‘=>1,‘offset‘=>1,‘rows‘=> 1,‘uid‘=>1,‘tag_id‘=>1,‘type‘=>1),	// 通过这种方法,描述需要传递参数,用于组装最终入cache的key
                     "desc"=>"描述",
                     "author"=>"ugg",
                     ), 

)
)

如上所示,通过一个算法,我们可以把site_top_feeds和params组装成唯一的入库key,组装后的key,大概是这样site_top_feeds_site_id=12&boutique=1&offset=0&rows=20&uid=&tag_id=0&type=2通过这种方式,我们避免工人自己组装key,从而杜绝第二种问题,同时在这个配置文件中,我们也设置了timeout,这样调用store时,我们可以直接从配置文件中读取,从而避免第三个问题。经过如上修改后,我们的cache方法,也做了适当的调整,调用示例如下。

$siteid = 121;
$seminal = 1;
$tag_id = 12;
$tag_id = 22;

$data =  fetch(‘site_top_feeds’,array(‘site_id‘=>$siteid,‘boutique‘=>$seminal, ‘offset‘=>"0", ‘rows‘ => "20", ‘uid‘ =>null,’tag_id’=>$tag_id,’type‘=>$type),‘feed‘);
if(empty($data)){
// db相关操作
$db = new Wk_DB();
    $data = $db->getTopFeeds($site_id,$seminal,0,20,null,$tag_id,$type);
//  $data数据其他处理逻辑 这里
……

$cache->store(‘site_top_feeds’,$data,array(‘site_id‘=>$siteid,‘boutique‘=>$seminal, ‘offset‘=>"0", ‘rows‘ => "20", ‘uid‘ =>null,’tag_id’=>$tag_id,’type‘=>$type),‘feed‘);
}

通过以上方案,我没看到,timeout超时时间没有了,key的组装也没有了,对于外层调用是透明的了。但是我们通过配置文件可以知道site_top_feeds的timeout是多少,通过封装的算法,知道组装的key是什么样的。
这种方式,并没有解决第一和第四的问题,封装性;要想完成封装性,第一件事情要做的就是回调函数,作为校本语言,PHP并没有完善的函数指针概念,当然要想执行一个函数其实也不需要指针。PHP支持回调函数的方法有两种call_user_func,call_user_func_array。
但是,我做了两个例子,发现上述方法,执行效率比原生方法,相差很多

native:0.0097959041595459s
call_user_func:0.028249025344849s
call_user_func_array:0.046605110168457s

例子代码如下:

$s = microtime(true);
for($i=0; $i< 10000 ; ++$i){
    $a = new a();
    $data = $a->aaa($array, $array, $array);
    $data = a::bbb($array, $array, $array);
}
$e = microtime(true);
echo "native:".($e-$s)."s\n";

$s = microtime(true);
for($i=0; $i< 10000 ; ++$i){
    $a = new a();
    $data = call_user_func(array($a,‘aaa‘),$array,$array,$array);
    $data = call_user_func(array(‘a‘,‘bbb‘),$array,$array,$array);
}
$e = microtime(true);
echo "call_user_func:".($e-$s)."s\n";

$s = microtime(true);
for($i=0; $i< 10000 ; ++$i){
    $a = new a();
    $data = call_user_func_array(array($a,‘aaa‘),array(&$array,&$array,&$array));
    $data = call_user_func_array(array(‘a‘,‘bbb‘),array(&$array,&$array,&$array));
}
$e = microtime(true);
echo “call_user_func_array:".($e-$s)."s\n";

在PHP中,知道一个对象和方法,其实调用方法很简单,比如上面的例子

$a = new a();
$data = $a->aaa($array, $array, $array);
$obj = $a;
$func = ‘aaa’;
$params = array($array,$array,$array);
$obj->$func($params[0],$params[1],$params[2]);	// 通过这种方式可以直接执行

这种方式的执行性能怎么样,经过我们对比测试发现

native:0.0092940330505371s
call_user_func:0.028635025024414s
call_user_func_array:0.048038959503174s
my_callback:0.11308288574219s

在加入大量方法策略验证中,性能损耗比较低,时间消耗仅是原生方法的1.25倍左右,远小于call_user_func的3倍多,call_user_func_array的5倍多,具体封装后的代码

switch(count($params)){
                case 0: $result = $obj->{$func}();break;
                case 1: $result = $obj->{$func}($params[0]);break;
                case 2: $result = $obj->{$func}($params[0],$params[1]);break;
                case 3: $result = $obj->{$func}($params[0],$params[1],$params[2]);break;
                case 4: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3]);break;
                case 5: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3],$params[4]);break;
                case 6: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3],$params[4],$params[5]);break;
                case 7: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3],$params[4],$params[5],$params[6]);break;
                default: $result = call_user_func_array(array($obj, $func), $params);  break;
            }

在使用这种方法之前,考虑过使用create_function来创建匿名函数,执行函数回调,经过测试create_function只能创造全局函数,不能创建类函数和对象函数,遂放弃。

完成以上准备工作后,就可以使用回调机制了,再次调用的业务代码

….
// 相关变量赋值
$db = new Wk_DB();
$callback[‘obj‘] = $db;
            $callback[‘func‘] = ‘getTopFeeds‘;
            $callback[‘params‘] = array(‘site_id‘=>$siteid,‘boutique‘=>$seminal, ‘offset‘=>"0", ‘rows‘ => "20", ‘uid‘ =>null,‘tag_id‘=>$tag_id,‘type‘=>$type);

            $top_feed_list = $cache->smart_fetch(‘site_top_feeds‘,$callback,‘feed‘);

使用以上方法实现对cache调用的封装,同时保证性能的高效,从而解决第一和第四个问题。

至此已经完成前四个问题,从而实现Cache的封装,并有效的避免了上面提到的第二,第三,第四个问题。但是对于第五个问题,dogpile问题,并没有解决,针对这种问题,最好的方式是在cache即将失效前,有一个进程主动触发db操作,获取DB数据放入Cache中,而其他进程正常从Cache中获取数据(因为此时cache并未失效);好在有Redis缓存,我们可以使用Redis的两个特性很好解决这个问题,先介绍下这两个接口
TTL方法:以秒为单位,返回给定 key 的剩余生存时间 (TTL, time to live),当 key 不存在时,返回 -2 。当 key 存在但没有设置剩余生存时间时,返回 -1 。否则,以秒为单位,返回 key 的剩余生存时间。很明显,通过这个方法,我们很容易知道key的还剩下的生存时间,通过这个方法,可以在key过期前做点事情,但是光有这个方法还不行,我们需要确保只有进程执行,而不是所有的进程都做,正好用到下面这个方法。
SETNX方法:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET) 的简写。返回值:设置成功,返回 1 。设置失败,返回 0 。通过这个方法,模拟分布式加锁,保证只有一个进程做执行,而其他的进程正常处理。结合以上Redis方法的特性,解决第五种的问题的,实例代码。

…
// 变量初始化
$key = “this is key”;
$expiration = 600;
$recalculate_at = 100;
$lock_length = 20;
$data = $cache->fetch($key);
$ttl = $cache->redis->ttl($key);
if($recalculate_at>=$ttl&&$r->setnx("lock:".$key,true)){
$r->expire(“lock:”.$key, $lock_length);
$db = new Wk_DB();
  $data = $db->getXXX();
  $cache->store($key, $expiration, $value);
}

解决方案

好了,关键核心代码如下
1:function回调部分代码

public static function callback($callback){
        // 安全检查
        if(!isset($callback[‘obj‘]) || !isset($callback[‘func‘])
            || !isset($callback[‘params‘]) || !is_array($callback[‘params‘])){
            throw new Exception("CallBack Array Error");
        }
        // 利用反射,判断对象和函数是否存在
        $obj = $callback[‘obj‘];
        $func = $callback[‘func‘];
        $params = $callback[‘params‘];
        // 方法判断
        $method = new ReflectionMethod($obj,$func);
        if(!$method){
            throw new Exception("CallBack Obj Not Find func");
        }   

        // 方法属性判断
        if (!($method->isPublic() || $method->isStatic())) {
            throw new Exception("CallBack Obj func Error");
        }   

        // 参数个数判断(不进行逐项检测)
        $paramsNum = $method->getNumberOfParameters();
        if($paramsNum < count($params)){
            throw new Exception("CallBack Obj Params Error");
        }   

        // 6个参数以内,逐个调用,超过6个,直接调用call_user_func_array
        $result = false;
        // 判断静态类方法
        if(!is_object($obj) && $method->isStatic()){
            switch(count($params)){
                case 0: $result = $obj::{$func}();break;
		case 1: $result = $obj::{$func}($params[0]);break;
                case 2: $result = $obj::{$func}($params[0],$params[1]);break;
                case 3: $result = $obj::{$func}($params[0],$params[1],$params[2]);break;
                case 4: $result = $obj::{$func}($params[0],$params[1],$params[2],$params[3]);break;
                case 5: $result = $obj::{$func}($params[0],$params[1],$params[2],$params[3],$params[4]);break;
                case 6: $result = $obj::{$func}($params[0],$params[1],$params[2],$params[3],$params[4],$params[5]);break;
                case 7: $result = $obj::{$func}($params[0],$params[1],$params[2],$params[3],$params[4],$params[5],$params[6]);break;
                default: $result = call_user_func_array(array($obj, $func), $params);  break;
            }
        }else{
            switch(count($params)){
                case 0: $result = $obj->{$func}();break;
                case 1: $result = $obj->{$func}($params[0]);break;
                case 2: $result = $obj->{$func}($params[0],$params[1]);break;
                case 3: $result = $obj->{$func}($params[0],$params[1],$params[2]);break;
                case 4: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3]);break;
                case 5: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3],$params[4]);break;
                case 6: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3],$params[4],$params[5]);break;
                case 7: $result = $obj->{$func}($params[0],$params[1],$params[2],$params[3],$params[4],$params[5],$params[6]);break;
                default: $result = call_user_func_array(array($obj, $func), $params);  break;
            }
        }

2:自动触发回调机制

public function smart_fetch($key,$callback,$namespace="wk") {
	key = $prefix.$key.$suffix;
        $result = $this->_redis->get($key);

        $bttl = false;
        // ttl状态判断(注意冷启动)
        if(!empty($ttl)){
            // 获得过期时间
            $rttl = $this->_redis->ttl($key);
            if($rttl > 0 && $ttl >= $rttl &&
                $this->_redis->setnx("lock".$key,true)){
                // 设置超时时间(超时时间3秒)
                $this->_redis->expire("lock".$key,3);
                $bttl = true;
            }
        }
	// 如何返回值不存在,调用回调函数,获取数值,并保持数据库
        if($bttl || !$result || (isset($CONFIG[‘FLUSH‘]) && !empty($CONFIG[‘FLUSH‘]))){
            // 重新调整参数
            $callbackparams = array();
            foreach($params as $k=>$value){
                $callbackparams[] = $value;
            }
            $callback[‘params‘] = $callbackparams;
            $result = Wk_Common::callback($callback);
            $expire = $key_config["timeout"];
            // 存储数据
            $status = $this->_redis->setex($key, $expire, $result);
            $result=$this->_redis->get($key);
        }

        // 删除锁
        if($bttl){
            $this->_redis->delete("lock".$key);
        }
        return $result;
    }

至此,我们使用脚本语言特性,通过user_call_func_array方法补齐所有函数回调机制,从而实现对Cache的封装,通过配置文件定义组装key的规则和每个key的超时时间,再通过Redis的ttl和setnx特性,保证只有一个进程执行DB操作,从而很好避免dogpile问题,实现cache自动触发,保证cache持续存在数据,并且有效减少DB的访问次数,提高性能。

时间: 2024-10-16 19:36:42

基于PHP的一种Cache回调与自动触发技术的相关文章

[ci] 基于1 上文实现拉取代码后能自动触发sonar-runner实现代码扫描评测,job1完成

基于1 上文实现拉取代码后能自动触发sonar-runner实现代码扫描评测,job1完成 ? 添加sonar插件 SonarQube Plugin ? 配置: 系统设置à告知jenkins,sonar在哪 ? https://docs.sonarqube.org/display/SONAR/User+Token How to Generate a Token To generate a token, to go?User > My Account > Security. Your exist

基于C#实现的自动化测试框架:发布自动触发自动化回归测试

接口自动化测试用例完成以后,以前都是发布以后手动运行测试用例.虽然手动运行下脚本也就是一个F5的事情,但是离自动化测试的标准差得很远.这两天有了个大胆的想法,想要实现以下发布时直接触发自动化回归测试用例的进行. 虽然说发布工作看起来很高大上,但操作实际上就是把要发布的内容扔到服务器下的指定文件夹里,然后去部署工具下切换路径,然后回收刷新即可进行操作.因此,只要实时监控服务器上的文件夹,一旦其中的文件发生改变,就直接启动运行对应的自动化测试用例,就能实现[版本发布时自动触发自动化测试]的需求. 一

基于Xen实现一种domain0和domainU的应用层数据交互高效机制

项目里有一个需求,domain0的应用层需要定时给domainU(hvm windows)的应用层传递一块数据,原来的方案是在domainU的应用层架设一个http服务器,监听在某个端口,然后需要塞数据时,domain0的应用程序连接该端口,并通过http send发送数据.发送完会等待domainU 的应用程序返回一个标记. 无意间看到这篇论文<全虚拟化HVM和半虚拟化PV虚拟平台通信机制分析>,里边介绍hvm情况下domainU与Doamin0用户层的数据交互机制,根据文章介绍,尝试设计一

基于Redis的三种分布式爬虫策略

前言: 爬虫是偏IO型的任务,分布式爬虫的实现难度比分布式计算和分布式存储简单得多. 个人以为分布式爬虫需要考虑的点主要有以下几个: 爬虫任务的统一调度 爬虫任务的统一去重 存储问题 速度问题 足够"健壮"的情况下实现起来越简单/方便越好 最好支持"断点续爬"功能 Python分布式爬虫比较常用的应该是scrapy框架加上Redis内存数据库,中间的调度任务等用scrapy-redis模块实现. 此处简单介绍一下基于Redis的三种分布式策略,其实它们之间还是很相似

基于Xen实现一种domain0和domainU的应用层数据交互高效机制 - 2

继续昨天的思路,今天先google了类似的实现domain0和domainU之间数据传输的方案 [Xen-devel] XenStore as a data transfer path?  这篇帖子讨论了xenstore作为domain0和domainU传递自定义数据的可行性,在Xen架构里,xenstore用于domain0和多个domainU之间传递控制数据,根据Xenstore的文档,不适合用于传输过大的数据,它的设计目标是对domain0的应用层有非常好的可视性和可操作性(tdb格式数据

基于Xen实现一种domain0和domainU的应用层数据交互高效机制 - 3

继续 上一篇 的研究,结合 xen4.2.3 的代码分析,发现 xen4.2.3 的应用层工具库 tools 包含一个工具叫 libvchan ,其头文件描述如下: * This is a library for inter-domain communication. A standard Xen ring 34 * buffer is used, with a datagram-based interface built on top. The grant 35 * reference and

《机器学习实战》基于信息论的三种决策树算法(ID3,C4.5,CART)

============================================================================================ <机器学习实战>系列博客是博主阅读<机器学习实战>这本书的笔记,包含对其中算法的理解和算法的Python代码实现 另外博主这里有机器学习实战这本书的所有算法源代码和算法所用到的源文件,有需要的留言 如需转载请注明出处,谢谢 ======================================

基于Android、iOS平台的移动端车牌识别技术及实现车牌识别过程

近年来,随着移动行业的爆发式发展,手机配置不断提高,基于手机平台的信息采集.图像处理.数据传输等方面的研究也成为了热点,这使得基于手机平台上的车牌识别成为可能.传统的车牌识别系统一般都基于固定的桌面平台.图像采集不灵活,特别是对于交通管理部门来说,对违章车辆车牌的自动登记非常不便,因此基于Android.iOS平台的移动端车牌识别技术出现了. 那么如何实现车牌识别的呢,下面简单说说: 首先对现存的车牌识别算法进行了研究,在诸多算法中寻找到一种适合在Android.iOS平台上运行的算法.先通过智

四种加载React数据的技术对比(Meteor 转)

1.四种加载React数据的技术对比(Meteor 转) : https://sanwen8.cn/p/31e4kdE.html 2. Meteor + Appolo   TelescopeJS/Telescope    https://github.com/TelescopeJS/Telescope/tree/devel http://nova-docs.telescopeapp.org/architecture.html