這篇文章主要討論的問題是:如何為項目設計一個完整而簡潔的緩存系統。只講做法,不講原理。在我們項目中,使用到了三種方法,來保證了緩存系統的有效簡潔。
1) 第一種,最常見的方式 讀取數據的主要步驟如下:
1)先從緩存中獲取數據(如果在緩存中獲取到,則直接返回已獲取的數據)
2)如果獲取不到,再從數據庫里面讀取相應的數據
3) 把獲取到的數據加入緩存中
注意:這種方式是在Model層,也就是業務處理層加入的。
實例代碼如下:
public static function getCombatPowerRank()
{
$cacheKey = 'Rank:CombatPower';
// 先從緩存中讀取
if ($list = F('Memcache')->get($cacheKey)) {
return $list;
}
$list = array();
// 遍歷所有用戶分庫,執行清理
for ($i = 1; $i <= DIST_USER_DB_NUM; $i++) {
if ($distList = Dao('Dist_User')->setDs($i)->getCombatPowerTopUsers(self::RANK_LIMIT)) {
$list = array_merge($list, $distList);
}
}
// 保存到緩存中
F('Memcache')->set($cacheKey, $list, C('RANK_CACHE_TIME'));
return $list;
}
這種方式確實很好理解,有一個弊端就是,所有的緩存都需要手動的加上以上緩存的代碼,需要修改函數的內部代碼。請注意,我們在項目中加入緩存的時間是項目完成的差不多了,也就是說需要有很多這樣的“讀取類”函數加入緩存,如果全是以上這種加入緩存方式的話,需要修改很多函數的內部代碼,那絕對是一個復雜而容易遺漏的苦力活。如果一不小心,就會出現錯誤。有沒有好的方式可以集中的給某些函數加入這樣的緩存系統呢(如果有的話,絕對是一個福音,哈哈)
2)第二種方式 ,在DAO層集中處理。在解釋這種方法之前,我先簡要說明一下我們的需求,便於更好理解為什么我可以這么做。
在我們的游戲項目中,有一部分數據時靜態資源數據,這種數據時配置好的,不會經常變動,每個用戶需要的都一樣。例如各種角色類的基礎的屬性,船只的基礎屬性等。這類數據涉及到的操作一般是讀:把一張表全部讀出來,獲取根據某個條件讀取相應的內容。既然操作單一,我們就直接在DAO層處理這類方法的緩存。做法就是給每一個Dao類里面的函數加入緩存。
不改變方法的內部代碼,卻可以給每個方法加入緩存,PHP魔術方法__call()就可以實現,如果對象調用某個方法,而這個方法又不存在,那么就會調用到這個魔術方法了,具體實現代碼如下:
/**
* 調用魔術方法
*
* @param string $method
* @param mixed $args
* @return mixed
*/
public function __call($method, $args)
{
if (! method_exists($this, '__CACHE__' . $method)) {
// 這里是實現數據庫鏈式查詢的,這里可以忽略
return parent::__call($method, $args);
}
$cacheKey = md5($this->_dbName . ':' . $this->_tableName . ':' . $method . ':' . serialize($args));
$data = $this->_cache->get($cacheKey);
if ($data === false) {
// 調用類里面的方法
$data = call_user_func_array(array($this, '__CACHE__' . $method), $args);
$this->_cache->set($cacheKey, $data);
}
return $data;
}
代碼運行機制: 比如說有這樣的一個調用關系:Dao('Static_Ship')->get(),但是在Static_Ship這個類中沒有get()這個方法,於是程序就會執行__call(),在這個類中,有一個這樣的方法__CACHE__get()這樣的一個方法,於是我就執行了這個方法,並且把這個函數的數據緩存起來了。這樣就達到了我們的目的,不改變函數內部的代碼,把函數的結果緩存起來。
3)集中處理和用戶有關的數據的緩存。如果大家細心的話,可以發現方法2中緩存的鍵值設計並不針對某一個用戶。
$cacheKey = md5($this->_dbName . ':' . $this->_tableName . ':' . $method . ':' . serialize($args)); 注意,這個鍵值的設計主要由庫名,表名,方法名,參數,需要注意的是庫名,因為如果數據庫涉及到分布式處理,就需要定位到相應的庫名中。如果需要緩存的數據和用戶有關系,我該如何設計呢。
這個處理方式還是需要結合需求,在我們項目中,需要讀取“我的船”相應的數據。比如
1)我需要讀取我的船的攻擊力:getShipFieldByUserShipId($uid, $shipId, attack)
2) 我需要讀取船的防御力 :getShipFieldByUserShipId($uid, $shipId, defence)
3) 讀取我的船的航海速度:getShipFieldByUserShipId($uid, $shipId, speed)
這個時候,有兩種SQL查詢方法:
1) uid = $uid AND shipId = $shipId AND field=$field
2) $data = " uid = $uid AND shipId = $shipId " 然后再這個$data數組中,返回相應的$data[$field].
你可能會覺得第二種方法會獲取到一些無用的數據,不好。但是,事實上,第二種方法比第一種方法好,因為他可以使用索引查詢,這個屬於SQL優化的,暫且不討論,第二個原因是便於方法可以加入緩存,查詢條件越“統一”,越容易加入緩存。第三種做法也是在DAO層中實現,緩存方式正是基於查詢條件高度統一的原則:
public function getField($pk, $field)
{
// 禁用緩存時
if (! $this->_isCached) {
return $this->field($field)
->where($this->_getPkCondition($pk))
->fetchOne();
}
$data = $this->get($pk);
return isset($data[$field]) ? $data[$field] : null;
}
get方法的主要代碼如下:
/**
* 根據主鍵 fetchRow
*
* @param mixed $pk
* @return array
*/
public function get($pk)
{
// 禁用緩存時
if (! $this->_isCached) {
return $this->where($this->_getPkCondition($pk))->fetchRow();
}
$cacheKey = $this->_getRowCacheKey($pk);
// 保證相同的靜態記錄只讀取一遍
if (isset($this->_rowDatas[$cacheKey])) {
return $this->_rowDatas[$cacheKey];
}
$row = $this->_cache->get($cacheKey);
if ($row === false) {
$row = $this->where($this->_getPkCondition($pk))->fetchRow() ?: array();
$this->_cache->set($cacheKey, $row, $this->_cacheTTL);
$this->_rowDatas[$cacheKey] = $row;
}
return $row;
}
獲取緩存鍵值的方法_getRowCacheKey()實現方式如下:
// 獲取單條記錄緩存key
protected function _getRowCacheKey($pk)
{
if (is_array($pk)) {
$pkString = implode(':', $pk);
}
else {
$pkString = $pk;
}
return md5($this->_dbName . ':' . $this->_tableName . ':get:' . $pkString);
}
保證查詢條件的高度統一,根據查詢的條件設置緩存,就是第三中做法的精髓了。
緩存系統需要注意的幾點:
1) 注意緩存系統的關聯性,如果數據發生了變化,一定要更新緩存
2)如果被緩存的數據和用戶有關,一定要把$cacheKey處理好,保證每個用戶數據不會被其它用戶串改。特別需要注意的是分庫的時候uid=1可不止一個哦
3)如果有必要的話,可以做一個緩存命中率的統計,統計哪些庫的那些表被哪些函數操作的次數
4) 如果某些表的數據頻繁的被修改,可以不需要緩存,如果用戶的行文記錄表,_isCached 這個屬性就是用來控制是否需要緩存。
見如下代碼:
/**
* 刪除(根據主鍵)
*
* @param mixed $pk
* @param array $extraWhere 格外的WHERE條件
* @return bool
*/
public function deleteByPk($pk, array $extraWhere = array())
{
$where = $this->_getPkCondition($pk);
if ($extraWhere) {
$where = array_merge($where, $extraWhere);
}
if (! $result = $this->where($where)->delete()) {
return $result;
}
// 清理緩存
if ($this->_isCached) {
$this->_deleteRowCache($pk);
}
// 統計Memcache讀寫次數
Dao('Massive_MemcacheRecord')->mark($this->_dbName, $this->_tableName, __METHOD__, 1);
return $result;
}
