概述
從以前的文章Yii源碼閱讀筆記 - 路由解析 及Yii源碼閱讀筆記 - Model層實現能夠看到Yii框架對於解析好的路由規則及數據表的schema都會根據條件嘗試讀寫緩存 來提升應用性能。html
但緩存組件並不是核心組件,須要額外的配置,默認ID爲cache
,若是不使用該ID,那麼就得注意同時配置好框架中使用緩存的組件。git
恰當地使用緩存組件,能明顯地提升應用的性能。github
針對不一樣的緩存後端(backend),Yii框架提供了多種緩存組件,如文件緩存(CFileCache)、Memcached緩存(CMemCache)、Redis緩存(CRedisCache)等。這些緩存組件(除CDummyCache外,CDummyCache並非一個有效的緩存組件)均直接繼承自抽象類CCache(見文件yii/framework/caching/CCache.php
)。算法
下面以使用Memcached緩存爲例,分析Yii框架緩存組件的實現。sql
分析
類CMemcache
所在的整個繼承樹(CMemcache
-> CCache
-> CApplicationComponent
-> CComponent
)上的類都沒有構造方法。數據庫
CMemcache
的init方法實現以下:後端
public function init()
{ // 調用父類CCache的init方法 parent::init(); // 獲取配置的Memcached服務器列表 $servers=$this->getServers(); // 獲取一個Memcache或Memcached對象 $cache=$this->getMemCache(); if(count($servers)) { // 將配置的Memcached服務器加到池中 foreach($servers as $server) { // 可選擇使用memcached php擴展,但最好別這樣,由於memcached擴展不支持一些有用的配置選項,從addServer方法的參數列表就能夠看出 // memcached擴展默認使用餘數分步哈希算法,但可配置使用一致性哈希算法 // 應使用memcache擴展 if($this->useMemcached) $cache->addServer($server->host,$server->port,$server->weight); else $cache->addServer($server->host,$server->port,$server->persistent,$server->weight,$server->timeout,$server->retryInterval,$server->status); } } // 若是沒有配置服務器列表,則默認使用localhost:11211 else $cache->addServer('localhost',11211); }
其中調用的父類的init方法實現以下:緩存
public function init()
{ parent::init(); // 能夠爲整個應用的緩存的key加一個統一的前綴,這是爲了不在同一個緩存池中,不一樣應用的key衝突 if($this->keyPrefix===null) // 若是沒有配置keyPrefix,則以應用的id做爲keyPrefix,這個key,能夠配置,若是未配置,則`sprintf('%x',crc32($this->getBasePath().$this->name))`這樣生成, // 其中name表示應用的名稱,可配置,默認爲「My Application」 // getId方法定義於抽象類CApplication中 $this->keyPrefix=Yii::app()->getId(); }
CMemcache
的init方法中調用的方法getServers
和getMemcache
,實現以下所示:服務器
public function getMemCache()
{ // 單例 if($this->_cache!==null) return $this->_cache; else { // 仍是用memcache吧 $extension=$this->useMemcached ? 'memcached' : 'memcache'; // 檢測一下是否記載了所需擴展 if(!extension_loaded($extension)) throw new CException(Yii::t('yii',"CMemCache requires PHP {extension} extension to be loaded.", array('{extension}'=>$extension))); // 實例化 return $this->_cache=$this->useMemcached ? new Memcached : new Memcache; } } public function getServers() { return $this->_servers; }
與getServers
對應的有方法setServers
,若是須要配置緩存服務器列表,則應該爲緩存組件配置servers
一項,基本形式爲:
'servers'=>array(
array( 'host'=>'127.0.0.1', 'port'=>11211, ), ),
setServers
方法實現以下:
public function setServers($config)
{ foreach($config as $c) $this->_servers[]=new CMemCacheServerConfiguration($c); }
其中實例化的類CMemCacheServerConfiguration
也定義於文件yii/framework/caching/CMemCache.php
中,其構造方法實現以下:
public function __construct($config)
{ if(is_array($config)) { foreach($config as $key=>$value) $this->$key=$value; if($this->host===null) throw new CException(Yii::t('yii','CMemCache server configuration must have "host" value.')); } else throw new CException(Yii::t('yii','CMemCache server configuration must be an array.')); }
從該構造方法能夠看到該類的做用就是將一個Memcached緩存服務器的配置信息封裝成一個配置類對象。該類有以下public的屬性:
/**
* @var string memcache server hostname or IP address */ public $host; /** * @var integer memcache server port */ public $port=11211; /** * @var boolean whether to use a persistent connection */ public $persistent=true; /** * @var integer probability of using this server among all servers. */ public $weight=1; /** * @var integer value in seconds which will be used for connecting to the server */ public $timeout=15; /** * @var integer how often a failed server will be retried (in seconds) */ public $retryInterval=15; /** * @var boolean if the server should be flagged as online upon a failure */ public $status=true;
這些屬性也便是每臺緩存服務器的可配置項,固然若是用的是memcached擴展,某些配置項就用不上了。
類CCache
定義了訪問控制類型public的方法get
、mget
、set
、add
、delete
、flush
,對應Yii框架緩存組件提供的幾個操做,即開發者可使用這些方法來操做緩存。
// 根據單個key,獲取對應的value
public function get($id) { // generateUniqueKey是根據$id來生成一個惟一的key,也即真正存到緩存的key並非get方法的$id參數值 $value = $this->getValue($this->generateUniqueKey($id)); // 若是設置爲不進行序列化,則直接返回取得的值 if($value===false || $this->serializer===false) return $value; // 若是未設置serializer,則說明存儲時使用的是默認的序列化方法,取到數據後對應地須要使用默認的方法進行反序列化 if($this->serializer===null) $value=unserialize($value); else // 不然,使用指定的方法進行反序列化 $value=call_user_func($this->serializer[1], $value); // 依賴檢查,若是依賴沒有變,則說明緩存有效, // 不然,返回false,表示緩存無效 // 咦,不要清掉無效的緩存項麼? if(is_array($value) && (!$value[1] instanceof ICacheDependency || !$value[1]->getHasChanged())) { Yii::trace('Serving "'.$id.'" from cache','system.caching.'.get_class($this)); return $value[0]; } else return false; } // 根據多個key值($ids),獲取多個value public function mget($ids) { $uids = array(); foreach ($ids as $id) $uids[$id] = $this->generateUniqueKey($id); $values = $this->getValues($uids); $results = array(); if($this->serializer === false) { foreach ($uids as $id => $uid) $results[$id] = isset($values[$uid]) ? $values[$uid] : false; } else { foreach($uids as $id => $uid) { $results[$id] = false; if(isset($values[$uid])) { $value = $this->serializer === null ? unserialize($values[$uid]) : call_user_func($this->serializer[1], $values[$uid]); if(is_array($value) && (!$value[1] instanceof ICacheDependency || !$value[1]->getHasChanged())) { Yii::trace('Serving "'.$id.'" from cache','system.caching.'.get_class($this)); $results[$id] = $value[0]; } } } } return $results; } // 向$id存儲一個元素值爲 $value public function set($id,$value,$expire=0,$dependency=null) { Yii::trace('Saving "'.$id.'" to cache','system.caching.'.get_class($this)); if ($dependency !== null && $this->serializer !== false) // 獲取依賴值 $dependency->evaluateDependency(); // 連同依賴一塊兒序列化而後緩存起來 // 下次獲取緩存後,檢查一下依賴是否發生變動,是則說明緩存已經失效 if ($this->serializer === null) $value = serialize(array($value,$dependency)); elseif ($this->serializer !== false) $value = call_user_func($this->serializer[0], array($value,$dependency)); return $this->setValue($this->generateUniqueKey($id), $value, $expire); } // 在緩存服務器以前不存在$id時, 以id做爲key存儲一個變量$value到緩存服務器 public function add($id,$value,$expire=0,$dependency=null) { Yii::trace('Adding "'.$id.'" to cache','system.caching.'.get_class($this)); if ($dependency !== null && $this->serializer !== false) $dependency->evaluateDependency(); if ($this->serializer === null) $value = serialize(array($value,$dependency)); elseif ($this->serializer !== false) $value = call_user_func($this->serializer[0], array($value,$dependency)); return $this->addValue($this->generateUniqueKey($id), $value, $expire); } // 根據id刪除緩存項 public function delete($id) { Yii::trace('Deleting "'.$id.'" from cache','system.caching.'.get_class($this)); return $this->deleteValue($this->generateUniqueKey($id)); } // 清空緩存 public function flush() { Yii::trace('Flushing cache','system.caching.'.get_class($this)); return $this->flushValues(); }
從上述代碼中能夠看到,每種操做方法實際上都是調用另外一個方法來完成操做:
- get -> getValue
- mget -> getValues
- set -> setValue
- add -> addValue
- delete -> deleteValue
- flush -> flushValues
但抽象類CCache
中對於後面的這些方法並無真正實現操做邏輯(除了getValues,其實現是循環調用getValue,也許並非開發者想要的實現,CMemCache
類重寫了這個方法),須要在繼承類中實現。CMemCache
類中對這些方法實現以下:
protected function getValue($key)
{ return $this->_cache->get($key); } protected function getValues($keys) { return $this->useMemcached ? $this->_cache->getMulti($keys) : $this->_cache->get($keys); } protected function setValue($key,$value,$expire) { // 注意:這個地方對於開發者來講也許是個坑 // 該方法的參數$expire並非一個時間點,而是一個時間間隔 // $expire = 0表示不會超時失效 if($expire>0) $expire+=time(); else $expire=0; // 使用memcache擴展時,add方法的那個額外參數值0,對應參數flag,表示是否對數據使用zlib進行壓縮 return $this->useMemcached ? $this->_cache->set($key,$value,$expire) : $this->_cache->set($key,$value,0,$expire); } protected function addValue($key,$value,$expire) { if($expire>0) $expire+=time(); else $expire=0; return $this->useMemcached ? $this->_cache->add($key,$value,$expire) : $this->_cache->add($key,$value,0,$expire); } protected function deleteValue($key) { return $this->_cache->delete($key, 0); } protected function flushValues() { return $this->_cache->flush(); }
方法get
、mget
、set
、add
、delete
、flush
的實現有兩點須要注意:
- 實際存儲的key並非方法調用時提供的key,而是通過方法
generateUniqueKey
處理的 - 實際存儲的value多是通過序列化的,並且可能還包含依賴值
關於第一點,generateUniqueKey
方法在抽象類CCache
中實現以下所示:
/**
* @param string $key a key identifying a value to be cached * @return string a key generated from the provided key which ensures the uniqueness across applications */ protected function generateUniqueKey($key) { return $this->hashKey ? md5($this->keyPrefix.$key) : $this->keyPrefix.$key; }
將本來的$key拼接上統一的前綴,若是須要,還進行md5哈希,這樣能保證不一樣的應用之間不會有key衝突。屬性hashKey默認值爲true。
關於第二點,緩存依賴的概念簡單來講就是在取到一個緩存項後,判斷該緩存項是否失效的一個條件。 以頁面緩存爲例,也許應用中在頁面模板渲染後並無直接將結果響應給用戶,而是先緩存起來,但頁面可能涉及一些動態內容,這些動態內容是從數據庫中某些數據生成的,爲了保證正確性,下次讀取頁面緩存後,還得去數據庫裏讀一下某些相關數據看是否有變動,,若是有變動,則須要從新渲染頁面模板,若是沒有變動,則直接將緩存的結果返回給用戶。這樣對於某些變動頻率不高的動態內容,在請求處理時就能夠避免沒必要要的頁面模板渲染過程。
判斷緩存依賴是否有變動的邏輯是:在寫緩存時,將當時緩存依賴的結果一併存入緩存,讀緩存的時候,再將最新緩存依賴的結果與以前存入緩存的依賴結果作對比,不相同,則說明有變動。
緩存依賴類須要實現接口ICacheDependency
,該接口聲明瞭兩個方法evaluateDependency
和getHasChanged
。
以緩存依賴類CDbCacheDependency
爲例(見文件yii/framework/caching/dependencies/CDbCacheDependency.php
), 該類直接繼承自類CCacheDependency
。類CDbCacheDependency
的做用就是根據一條SQL語句從數據庫查詢數據,而後根據查詢結果來判斷緩存是否有效。
父類CCacheDependency
中實現方法evaluateDependency
和getHasChanged
,以下所示:
/**
* Evaluates the dependency by generating and saving the data related with dependency. * This method is invoked by cache before writing data into it. */ public function evaluateDependency() { // 判斷是否複用緩存依賴結果 // 默認爲false,可在實例化緩存依賴類時設置 // 另外對於PHP來講,這個「複用」也只能是一次請求處理過程當中的複用 if ($this->reuseDependentData) { // hash方法:求當前對象序列化結果的sha1哈希值 $hash=$this->getHash(); if(!isset(self::$_reusableData[$hash]['dependentData'])) // 若是沒有結果可複用,則得從新生成 self::$_reusableData[$hash]['dependentData']=$this->generateDependentData(); $this->_data=self::$_reusableData[$hash]['dependentData']; } else $this->_data=$this->generateDependentData(); } /** * @return boolean whether the dependency has changed. */ // 這個方法實際上是讀緩存時,從緩存數據中取出緩存依賴的部分反序列化後獲得一個依賴對象,由該依賴對象調用它的這個方法來判斷緩存依賴是否有變動, // 因此它的_data屬性是寫緩存時的緩存依賴數據 public function getHasChanged() { if ($this->reuseDependentData) { $hash=$this->getHash(); if(!isset(self::$_reusableData[$hash]['dependentData'])) self::$_reusableData[$hash]['dependentData']=$this->generateDependentData(); // 不相等,則說明發生了變動 return self::$_reusableData[$hash]['dependentData']!=$this->_data; } else return $this->generateDependentData()!=$this->_data; }
但父類CCacheDependency
並未有效實現上述兩個方法中調用的generateDependentData
方法,在類CDbCacheDependency
中實現以下:
protected function generateDependentData()
{ if($this->sql!==null) { // 獲取數據庫鏈接組件對象 $db=$this->getDbConnection(); // 準備SQL執行,其中sql屬性在構造方法中賦值 $command=$db->createCommand($this->sql); if(is_array($this->params)) { // 綁定參數 foreach($this->params as $name=>$value) $command->bindValue($name,$value); } // 避免從緩存中讀取數據庫查詢結果 if($db->queryCachingDuration>0) { // temporarily disable and re-enable query caching $duration=$db->queryCachingDuration; $db->queryCachingDuration=0; $result=$command->queryRow(); $db->queryCachingDuration=$duration; } else $result=$command->queryRow(); return $result; } else throw new CException(Yii::t('yii','CDbCacheDependency.sql cannot be empty.')); }