在資料讀多寫少的情況下作為緩存來使用,恐怕是Redis使用最普遍的場景了。當使用Redis作為緩存的時候,一般流程是這樣的。
- 如果緩存在Redis中存在,即緩存命中,則直接傳回資料
- 如果Redis中沒有對應緩存,則需要直接查詢資料庫,然後存入Redis,最後把資料傳回
通常情況下,我們會為某個緩存設定一個key值,并針對key值設定一個過期時間,如果被查詢的資料對應的key過期了,則直接查詢資料庫,并将查詢得到的資料存入Redis,然後重置過期時間,最後将資料傳回,僞代碼如下:
/**
* 根據使用者名擷取使用者詳細資訊
* @author 公衆号【蟬沐風】
*/
public User getUserInfo(String userName) {
User user = redisCache.getName("user:" + userName);
if (user != null) {
return user;
}
// 從資料庫中直接搜尋
user = selectUserByUserName(userName);
// 将資料寫入Redis,并設定過期時間
redisCache.set("user:" + userName, user, 30000);
// 傳回資料
return user;
}
一緻性問題
但是,在Redis的key值未過期的情況下,使用者修改了個人資訊,我們此時既要操作資料庫資料,也要操作Redis資料。現在我們面臨了兩種選擇:
- 先操作Redis的資料,再操作資料庫的資料
- 先操作資料庫的資料,再操作Redis的資料
如論選擇哪種方法,最理想的情況下,兩個操作要麼同時成功,要麼同時失敗,否則就會出現Redis和資料庫資料不一緻的情況。
遺憾的是,目前沒有什麼架構能夠保證Redis的資料和資料庫的資料的完全一緻性。我們隻能根據場景和所需要付出的代碼來采取一定的措施降低資料不一緻出現的機率,在一緻性和性能之間取得一個折中。
下面我們來讨論一下關于Redis和資料庫之間資料一緻性的一些方案。
方案選擇
是删除緩存還是更新緩存?
當資料庫資料發生變化的時候,Redis的資料也需要進行相應的操作,那麼這個「操作」到底是用「更新」還是用「删除」呢?
「更新」的話調用Redis的set方法,新值替換舊值;「删除」直接删除原來的緩存,下次查詢的時候重新讀取資料庫,然後再更新Redis。
結論:推薦直接使用「删除」操作。
因為使用「更新」操作的話,你會面臨兩種選擇
- 先更新緩存,再更新資料庫
- 先更新資料庫,再更新緩存
第1種不用考慮了,下面讨論一下「先更新資料庫,再更新緩存」這種方案。
如果線程1和線程2同時進行更新操作,但是每個線程的執行順序如上圖所示,此時就會導緻資料不一緻,是以從這個角度上我們推薦直接使用删除緩存的方式。
此外,推薦使用「删除緩存」還有兩點原因。
- 如果寫資料庫的場景比讀資料場景多,采用這種方案就會導緻緩存就被頻繁寫入,浪費性能;
- 如果緩存要經過一系列複雜的計算才能得到,那麼每次寫入資料庫後,都再次計算寫入的緩存無疑也是浪費性能的。
明确這個問題之後,擺在我們面前的就隻有兩個選擇了:
- 先更新資料庫,再删除緩存
- 先删除緩存,再更新資料庫
先更新資料庫,再删除緩存
這種方式可能存在以下兩種異常情況
- 更新資料庫失敗,這時可以通過程式捕獲異常,直接傳回結果,不再繼續删除緩存,是以不會出現資料不一緻的問題
- 更新資料庫成功,删除緩存失敗。導緻資料庫是最新資料,緩存中的是舊資料,資料不一緻
第2種情況應該怎麼辦呢?我們有兩種方式:失敗重試和異步更新。
失敗重試
如果删除緩存失敗,我們可以捕獲這個異常,把需要删除的 key 發送到消息隊列。自己建立一個消費者消費,嘗試再次删除這個 key,直到删除成功為止。
這種方式有個缺點,首先會對業務代碼造成入侵,其次引入了消息隊列,增加了系統的不确定性。
異步更新緩存
因為更新資料庫時會往 binlog 中寫入日志,是以我們可以啟動一個監聽 binlog變化的服務(比如使用阿裡的 canal開源元件),然後在用戶端完成删除 key 的操作。如果删除失敗的話,再發送到消息隊列。
總結
總之,對于删除緩存失敗的情況,我們的做法是不斷地重試删除操作,直到成功。無論是重試還是異步删除,都是最終一緻性的思想。
先删除緩存,再更新資料庫
這種方式可能存在以下兩種異常情況
- 删除緩存失敗,這時可以通過程式捕獲異常,直接傳回結果,不再繼續更新資料庫,是以不會出現資料不一緻的問題
- 删除緩存成功,更新資料庫失敗。在多線程下可能會出現資料不一緻的問題
這時,Redis中存儲的舊資料,資料庫的值是新資料,導緻資料不一緻。這時我們可以采用延時雙删的政策,即更新資料庫資料之後,再删除一次緩存。
用僞代碼表示就是:
/**
* 延時雙删
* @author 公衆号【蟬沐風】
*/
public void update(String key, Object data) {
// 首先删除緩存
redisCache.delKey(key);
// 更新資料庫
db.updateData(data);
// 休眠一段時間,時間依據資料的讀取耗費的時間而定
Thread.sleep(500);
// 再次删除緩存
redisCache.delKey(key);
}
最後給讀者留下兩個思考題:
- 為什麼先更新緩存,再更新資料庫行不通?
- 延時雙删的方法為什麼要休眠一段時間呢?
歡迎大家評論區留言。