Yii源碼閱讀筆記 - 緩存

Yii源碼閱讀筆記 - 緩存

2014-11-19 三php

By youngsterxyf

概述

從以前的文章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方法中調用的方法getServersgetMemcache,實現以下所示:服務器

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的方法getmgetsetadddeleteflush,對應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(); } 

方法getmgetsetadddeleteflush的實現有兩點須要注意:

  1. 實際存儲的key並非方法調用時提供的key,而是通過方法generateUniqueKey處理的
  2. 實際存儲的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,該接口聲明瞭兩個方法evaluateDependencygetHasChanged

以緩存依賴類CDbCacheDependency爲例(見文件yii/framework/caching/dependencies/CDbCacheDependency.php), 該類直接繼承自類CCacheDependency。類CDbCacheDependency的做用就是根據一條SQL語句從數據庫查詢數據,而後根據查詢結果來判斷緩存是否有效。

父類CCacheDependency中實現方法evaluateDependencygetHasChanged,以下所示:

/**
 * 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.')); }
相關文章
相關標籤/搜索