緩存驚羣現象,在各種緩存中都會存在這種現象,這裏以Redis爲例,提供一種解決思路,留作參考~
首先,所謂的緩存過期引起的“驚羣”現象是指,在大併發情況下,我們通常會用緩存來給數據庫分壓,但是會有這麼一種情況發生,那就是當一個緩存數據失效之後會導致同時有多個併發線程去向後端數據庫發起請求去獲取同一個數據,這樣如果在一段時間內同時生成了大量的緩存,然後在另外一段時間內又有大量的緩存失效,這樣就會導致後端數據庫的壓力突然增大,這種現象就可以稱爲“緩存過期產生的驚羣現象”!
以下代碼的思路,就是利用“鎖機制”來防止驚羣現象。先看代碼:
class KomaRedis{ private $redis; //redis對象 private static $_instance = null; private function __construct($config = array()) { if (empty($config)) { return false; } $this->redis = new Redis(); $this->redis->connect($config['server'], $config['port']); return $this->redis; } /** * @param array $config * @return redis操作類對象 */ public static function getInstance($config = array()) { if (!(self::$_instance instanceof self)) { self::$_instance = new self ($config); } return self::$_instance; } /** * 獲取緩存 * @param $key string $name * @return array,object,number,string,boolean * @desc 此方法使用了鎖機制來防止防止緩存過期時所產生的驚羣現象,保證只有一個進程不獲取數據,可以更新,其他進程仍然獲取過期數據 */ public function getByLock($key) { $sth = $this->redis->get($key); if ($sth === false) { return $sth; } else { $sth = json_decode($sth, TRUE); if (intval($sth['expire']) <= time()) { $lock = $this->redis->incr($key . ".lock"); if ($lock === 1) { return false; } else { return $sth['data']; } } else { return $sth['data']; } } } /** * 設置緩存 * @param $key string $name 緩存鍵 * @param $value $string ,array,object,number,boolean $value 緩存值 * @param null $ttl $string ,number $ttl 過期時間,如果不設置,則使用默認時間,如果爲 infinity 則爲永久保存 * @return bool * @desc 此方法存儲的數據會自動加入一些其他數據來避免驚羣現象,如需保存原始數據,請使用 set */ public function setByLock($key, $value, $ttl = null) { if (is_numeric($ttl) && intval($ttl) > 0) { $ttl = intval($ttl); $exp = time() + $ttl; $arg = array("data" => $value, "expire" => $exp); } else { $ttl = 300; $exp = time() + $ttl; } empty($ttl) OR $ttl += 300; //增加redis緩存時間,使程序有足夠的時間生成緩存 $arg = array("data" => $value, "expire" => $exp); $rs = $this->redis->setex($key, $ttl, json_encode($arg, TRUE)); $this->redis->del($key . ".lock"); return $rs; } /** * 返回redis對象 * redis有非常多的操作方法,我們只封裝了一部分 * 拿着這個對象就可以直接調用redis自身方法 */ public function redis() { return $this->redis; } }
原理就是:
首先,在存儲數據的時候,設置數據的過期時間比實際設置的過期時間多300秒,然後存儲的數據中,通過一個數組來存儲數據,數組中一個鍵用來存放真實的數據,另外一個鍵用來存放數據的真實過期時間,這個留到後期獲取數據的時候做校驗,然後把對應這個數據的“鎖”刪除掉。
這裏這麼做的原因和讀取數據的做法相關!
然後,在讀取數據的時候,依然像平時一樣直接讀取,如果數據已經超過了有效期(注意:這裏的有效期並非設置的有效期,而是更該之後的有效期),那麼就只能去讀後端數據庫。如果數據依然有效,則需要去判斷,判斷數據“在真正的有效期內是否失效”,如果沒有失效,則直接返回數據!
重點是,假如數據“在僞造的有效期內沒有失效,而在真正的有效期內已經失效”,那麼這時就需要去判斷“數據的鎖”!
通過代碼“$lock = $this->redis->incr($key . ".lock");”可以獲取數據的鎖,“$lock === 1”表示數據沒有鎖,那麼這一次請求需要發送到後端數據庫去讀取最新的數據,否則的話表示該數據已經加了鎖,也就是已經有一個線程去後端讀取數據了,那麼後來的線程也就沒有權限再去後端取數據,需要等到前面的那個線程執行結束,但是這次讀取就只能讀取“舊的數據”了!
通過上面的解釋也就明白,爲什麼在存儲數據的時候需要“刪除數據的鎖”!因爲一旦數據被重新存儲,那麼說明已經有一個線程去後端得到了最新的數據,那麼該數據的鎖就可以釋放,然後下一個線程在獲取數據的時候如果有需要就可以得到這個鎖,然後纔有權限進入到後端去讀取新數據!