從程式碼結構優化redis快取的方式, 還在為多人開發專案時混亂的redis key煩惱嘛?
我們目前資料層次按照效能來分 頂層 : redis 中間層: es 底層: mysql
目前針對一些比較繁瑣棘手的, 但是可以花點時間就能解決的問題, 就是在redis中的快取問題
雖然目前業務量不是很大, 但是程式碼中會看到大家習慣性的為了提升效能而寫出如下的程式碼
! 其中出現過一次redis崩潰事故
問題點: 1.為了不被快取影響, 很多時候會在if的時候加上true,這樣反覆修改很麻煩,且出現過幾次忘記改回來
2. 當線上出現事故是, 修復資料後依然沒效果, 多數因為快取導致,3. dao層資料處理不夠統一化, 不同的開發者都根據自己的想法做快取, 清理起來需要人工找到對應的key, 並手動執行del方法很是麻煩
4. 有好幾個線上問題, 被之前寫的自定義快取key給坑慘了. 因為這種情況一旦出了問題, 定位並解決後發現沒效果, 然後又花了很多時間來定位快取位置.
之前程式碼的寫法: /** * 獲取使用者地址所有資訊 * @param $addressId * @return $this|mixed */ public function getAllList($type) { $ckey = self::cache()->genKey(self::CACHE_KEY_INFO_ALL, $type); $arrRet = self::cache()->forceGet($ckey); if ($arrRet === false) { $arrRet = self::find()->where('type', $type)->asArray()->all(); self::cache()->forceSet($ckey, $arrRet); } return $arrRet; }
♦ 解決方案:1. 針對後面新加的dao層程式碼, 儘量做到資料統一,更換key字首的形成方式,為方法名
2. ORM本來就是那一個比較好用的封裝方式了, 其中的一些的before after 用起來,把快取統一就能解決很多問題
其實這裡也只是用了ORM的用了所有dao層類繼承的一個基礎類, 在父類中統一了可以的set和get
(快取的key現在根據類名, 方法名為字首 特殊id為字尾, 不需要在手動設定key名 注: 針對已經寫完的上線程式碼, 沒法統一起來成本很高,暫不考慮)
新方案的寫法:
使用出直接修改為:
$this->cacheOn(__FUNCTION__, $type); 即可,其他部分均已在orm中實現
例子:
/**
* 獲取使用者地址所有資訊
* @param $addressId
* @return $this|mixed
*/
public function getAllList($type)
{
$this->cacheOn(__FUNCTION__, $type); //__FUNCTION__當前方法常量
return self::find()->where('type', $type)->asArray()->all();
}
實現詳解:
1. 在cacheOn中把, 通過沒檔案統一控制快取
2. 通過 $this->needCache控制快取不錯在時set快取
3. 在$this->_afterFind() 中如果開啟快取,則把資料返回寫入到$this->needCache所儲存的key中
4. 通過del($key."*")的方式一次性更新本類的所有快取, set的時間複雜度從O(n) 提升到O(xn) x為常量, 基本不影向性能,
多了一個keys搜尋操作, 在超大redis資料中, keys方法影響效能, 目前也不考慮
實際程式碼:
/**
* 統一dao層開啟快取方式, 前提是配置檔案['debug']['isCache']為true
* 注意: 只能在查詢的上一步,快取開啟,不允許巢狀快取
* @param $function string dao層呼叫的常量__FUNCTION__必須填固定常量
* @param $key
* @return bool
* @throws Yaf_Exception
*/
protected function cacheOn($function, $key)
{
$config = Utils_Common::getConfig(); //快取配置
if (!isset($config['debug']['isCache'])) {
throw new Yaf_Exception('isCache不能為空', -10000);
}
$isCache = $config['debug']['isCache'];
if ($isCache === true) {
//key由class function 和特殊id組成
$key = self::cache()->getKey(__CLASS__.$function, $key);
if ($ret = self::cache()->get($key) !== false) {
return $ret;
} else {
$this->needCache = $key; //表示我是需要快取的
}
}
}
/**
* 查詢之後執行
* @param $models
*/
protected function _afterFind($models)
{
if (!empty($this->needCache)) {
self::cache()->forceSet($this->needCache, $models);
}
}
快取清空問題:
直接使用del key*的方式清空本類的快取
/**
* 更新之前執行
* @param $attributes
*/
protected function _beforeUpdate($attributes)
{
//redis的set次數時間複雜度從O(n)提升到O(xn),不會影響效能 x 常量可忽略, n為更新次數, 這種方案不適合秒殺
//刪除該類的所有redis快取
$keys = $this->cache()->keys(__CLASS__.'*');
$this->cache()->del($keys);
}
!!!!!!!!: 這裡可能會看著奇怪, redis set的時候沒有設定過期時間, 其實只是我們封裝了一層, forsetSet設定了預設過期時間3600s
!!!!!!!!: 這種設計方案是, 當一個dao層的任何一個數據有更新, 就要刪除這個Dao類名字首的所有redis, key 這種方案是有缺陷的,
在秒殺環境下就會出現直接打到mysql的情況, 當然解決方案也很簡單, 就是這裡統一封裝的時候, 在強制規定上指定的id即可, 比如userId, couponId等方式.
效能問題的解決方案: 我綜合當前系統得出的解決方案中, keys造成了效能問題, 其實只要再做一個簡單的快取就能迎刃而解, 用redis的sadd集合把改dao類名字首的key儲存起來, 相當於把keys做了一個索引, 見程式碼:
self::cache()->sAdd(__CLASS__, self::$needCache); //使用者索引存了什麼key
$keys = $this->cache()->sGetMembers(__CLASS__);
替代
$keys = $this->cache()->keys(__CLASS__.'*');
/**
* 查詢之後執行
* @param $models
*/
protected function _afterFind($models)
{
if (!empty(self::$needCache)) {
self::cache()->sAdd(__CLASS__, self::$needCache);
self::cache()->set(self::$needCache, $models);
self::$needCache = '';
}
}
/**
* 更新之前執行
* @param $attributes
*/
protected function _beforeUpdate($attributes)
{
//redis的set次數時間複雜度從O(n)提升到O(xn),不會影響效能 x 常量可忽略, n為資料的更新次數
//刪除該類的所有redis快取
$keys = $this->cache()->sGetMembers(__CLASS__);
// $keys = $this->cache()->keys(__CLASS__.'*');
$this->cache()->del($keys);
}
這樣這個點上的問題也能解決了
東西不多, 主要從一個封裝層面做了一下redis快取key的統一化, 在現有基礎思想上, 還可以做很多優化,
簡單的幾步, 就不用再為一些找不到的redis key煩惱了