上一篇文章:緩存的正確打開方式(一) 中介紹了讀取緩存時的一些細節,有讀就有寫,本篇來聊聊,當我們需要更新緩存該怎麼做?
當我們通過一些方式:如後臺管理系統更新了相關的數據信息,或者用戶在一些操作的時候更新了一些數據信息,如果這些信息正好也在緩存裏,那一般也需要在更新數據庫的時候,也更新緩存.
那更新的流程是什麼呢?很多人可能覺的很簡單,示例如下?
public function setData($data)
{
//更新數據庫
$db->update($data);
//更新緩存
$redis->set($key, $data);
return true;
}
嗯,咋一看,沒毛病,但真的是這樣麼?
場景一:如果更新數據庫失敗了?
結果可有兩個
-
更新數據庫拋異常了,中斷,緩存也不影響
-
數據庫不拋異常,緩存繼續更新,結果會導致 數據庫和緩存不一致
-
緩存更新失敗,數據庫和緩存不一致
如果是第二種情況,可能就會帶來線上的bug了,這時同學可能會有如下的優化:
public function setData($data)
{
try {
//更新數據
$ret = $db->update($data);
if($ret) {
//數據庫更新成功,則更新緩存
$redis->set($key, $data);
}
return true;
}catch (Throwable $e) {
//TODO 異常處理
}
}
針對第三種情況,好像只能不斷的重試了.
假設我們操作redis比較正常,但這樣就OK了麼?
場景二:併發更新的問題?
假如兩個請求在併發操作相同的一條數據,由於db的update和cache的set並不是原子性的,所以存在下面的時序可能性:
-
db 更新了 data1
-
db 更新了 data2
-
cache緩存了data2
-
cache 緩存了 data1
這樣就造成了緩存裏的數據是老數據(data1),從而導致緩存與數據庫不一致
那怎麼處理呢?
有同學可能會說,我先set cache ,再 update db呢?
問題或許更嚴重,db操作失敗的概率可能大於 cache 操作的概率,這樣可能導致更多數據不一致的情況
如果要嚴格的要求更新數據庫後,緩存能實時的一致更新 ,確實沒有完美的的方案,上述場景中,第二種屬於邏輯上的bug,碰到概率比較高,所以我們可以優化一下 ,讓不一致的情況變的更少
優化一:set cache 變delete cache
public function setData($data)
{
try {
//更新數據
$ret = $db->update($data);
if($ret) {
//數據庫更新成功,則更新緩存
$redis->delete($key, $data);
}
return true;
}catch (Throwable $e) {
//TODO 異常處理
}
}
這樣的話,併發更新的問題就不存在了,如以下時序:
-
db 更新了 data1
-
db 更新了 data2
-
cache刪除了data2
-
cache 刪除了 data1
都是刪除cache,都是會回源到db拉到最新數據
(另一個問題:如果先delete cache再update db, 會有什麼問題,歡迎留言)
那這個方式是不是就完美了呢?並不了,還存在一些極端的問題,看如下場景:
-
請求1讀取緩存
-
緩存失效,回源數據庫
-
請求2 更新db
-
請求2 刪除cache
-
請求1 設置cache
這樣也導致cache是老數據,但這種場景概率還是很低的(需滿足緩存失效,讀取db比update db時間還要長)
優化二:異步更新
可以把緩存更新的放到一個異步對列裏,進行異步更新,這種方式會帶來幾個問題
1、邏輯變得更重
2、又引入了一個新的隊列依賴
如果不用消息隊列,是否可行?
也是可行的,可以直接通過db的binlog進行更新
總結:
利用緩存,本身就要做好數據不一致的預期,但我們還是可以通過細節的把握,讓數據不一致的情況儘可能減少。
最後用一張圖對比一下:
(思考下最後一種方式帶來什麼更好的改進?)
下一篇我們來聊聊用redis做鎖的一些細節