基於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);// 判斷dataif(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.0097959041595459scall_user_func:0.028249025344849scall_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.0092940330505371scall_user_func:0.028635025024414scall_user_func_array:0.048038959503174smy_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的訪問次數,提高效能。
http://www.bkjia.com/PHPjc/925224.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/925224.htmlTechArticle基於PHP的一種Cache回調與自動觸發技術 背景 在PHP中使用Memcache或者Redis時,我們一般都會對Memcache和Redis封裝一下,單獨完成寫一個Cache類,...