使用memcache處理緩存的三種方案


這篇文章主要討論的問題是:如何為項目設計一個完整而簡潔的緩存系統。只講做法,不講原理。在我們項目中,使用到了三種方法,來保證了緩存系統的有效簡潔。

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;
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM