引言
自Redis入門篇過後,已經好久沒更Redis了,接下來應該從實戰篇,原理篇,面試篇幾個層次來展開,本篇主要是實戰篇中的資料庫緩存一緻性問題,以問題形式展開,應對面試場景作答【melo稱其為"手撕面答"】,盡量簡短,某些部分可能不會進行詳細介紹。
🎨本篇腦圖速覽
🎯為什麼是删除緩存而不是更新緩存?
🎐懶加載
一種懶加載的思想,因為每次更改資料之後,不一定立馬就有人來用。 若更新的次數遠大于讀取的次數,此時會頻繁更新緩存,但一直沒人使用,若緩存更新的成本很高的話,此時會非常浪費性能資源。
🎈并發更新的情況
ABBA 【A的操作過程中,穿插了B的完整過程】
- A更新資料庫為1
- B更新資料庫為2
- B更新緩存為2
- A更新緩存為1
最後導緻資料庫最終是2,但緩存是1,也就是B的緩存更新丢失了
🎯為什麼要先更新資料庫,再删除緩存
若先删除緩存,再更新資料庫的話:
- A線程删除緩存
- B線程查詢資料,緩存中沒有了,會去計算資料庫,設定緩存【此時計算出來的緩存是舊資料,是資料庫更新之前的資料】
- A線程再更新資料庫
接下來的查詢,一直走的是緩存,也就是舊資料,這樣就出現了資料庫和緩存中資料不一緻的情況
若先更新資料庫,再删除緩存的話
- A線程更新資料庫
- B線程查詢資料,此時還未删除緩存,緩存中還有,得到的就是舊的緩存資料【此時就出現了資料庫和緩存中資料不一緻的情況】
- A線程再删除緩存
A讀B寫【先更後删可能會有資料不一緻的情況,但很少見】
- A讀取,發現緩存剛好過期了
- A查詢出資料庫的舊值,在設定緩存之前
- B更新資料庫,并删除了緩存
- A用舊值,設定了緩存【緩存中是舊的資料,資料庫是新的】
但由于緩存的寫入要快于MySQL的寫入,一般不可能在2跟4之間,穿插一個3操作【3還操作了資料庫】
- 如果順序是【2,4,3】的話,A設定了緩存之後也沒關系,因為後續B還會再次删除緩存
- 之後的查詢,發現緩存過期會去資料庫中查詢得到最新的資料
🎯🎈如何解決A讀B寫時先更後删的極端情況?
延時雙删
- A讀取,發現緩存失效了,此時去查詢資料庫【查出舊值】
- B更新資料庫
- B設定新緩存
- A設定緩存為舊值
到這一步,如果之後的查詢,查到的都會是舊的緩存,是以我們可以
- 延時500毫秒
延時500毫秒是為了讓B能夠更新完資料庫,我們再次查詢資料庫才能獲得最新的值,來設定緩存
- 删除緩存
🎯先删除緩存,後更新資料庫如果也出現類似上邊的問題怎麼辦?
假設一個場景:
- 請求A進行寫操作,删除緩存
- 請求B查詢發現緩存不存在
- 請求B去資料庫查詢得到舊值
- 請求B将舊值寫入緩存
- 請求A将新值寫入資料庫
這時候緩存中的資料就是髒資料,如果緩存沒有設定過期時間的話,就導緻這個資料在下一次修改之前傳回給使用者的都是髒資料。
🎯從後者操作可能失敗的角度來看,選擇哪種政策更好一點?
先更新後删除緩存的話,後者失敗,情況是這樣的:
- A更新資料庫,然後緩存沒有删除成功
- B查詢,直接走了緩存【舊資料】
這就出現了緩存跟資料庫不一緻的情況
先删除緩存後更新資料庫的話,後者失敗,情況是這樣的:
- A删除緩存,然後沒能更新資料庫
- B查詢,發現緩存沒有,則去查詢資料庫,設定到緩存裡邊
此時并不會出現緩存跟資料庫不一緻的情況,因為A還沒來得及更新,這樣資料庫跟緩存中,一直都是舊的資料,但至少不會出現資料不一緻的問題
- 是以如果從這個角度來看的話,先删除緩存,後更新資料庫才是更優解。
🎯🎈如何解決後者失敗的情況呢?
使用消息隊列重試
- 請求 A 先對資料庫進行更新操作,同時吧
- 在對 Redis 進行删除操作的時候發現報錯,删除失敗
- 此時将Redis 的 key 作為消息體發送到消息隊列中
- 系統接收到消息隊列發送的消息後再次對 Redis 進行删除操作
但是這個方案會有一個缺點就是會對業務代碼造成大量的侵入,深深的耦合在一起。
有沒有必要更新成功後就投遞到mq呢?而不是等到redis删除失敗了再投遞到mq
如果考慮Java項目程序在更新資料庫之後就當機了的話,那無論哪種都沒法避免緩存删除的失敗 如果硬摳的話,更新成功後,到删除redis還有一段間隙,這個間隙先投遞到mq,會更有保障一點,但相應的編碼規則也更麻煩了點
- 更新資料庫成功,投遞到mq表示要删除 某個key
- 業務繼續執行,删除redis:
- 删除成功的話,需要删除掉mq裡邊的消息,防止重複消費
- 删除失敗的話,就需要消費者拉取mq,再次删除緩存,實作重試機制,當然,如果重試超過的一定次數,還是沒有成功,我們就需要向業務層發送報錯資訊了。後續的處理可能就先丢到死信隊列裡邊了。
可以發現編碼确實麻煩了些,因為要考慮重複消費的問題。那麼如何解決呢?Java崩了,咱下遊還有 mysql 和 mq 沒崩呢!也就是以引出了下文的訂閱binlog日志的方法
訂閱binlog日志
原理:更新資料庫成功,就會産生一條變更日志,記錄在 binlog 裡。
于是我們就可以通過訂閱 binlog 日志,拿到具體要操作的資料,然後再執行緩存删除,阿裡巴巴開源的 Canal 中間件就是基于這個實作的。
具體流程:
- canal模拟mysql slave的互動協定,僞裝自己為mysql slave,向mysql master發送dump協定;
- mysql master收到dump請求,開始推送binary log給slave(也就是canal);
- canal解析binary log對象(原始為byte流);
- canal将解析後的對象,根據業務場景,分發到比如 MySQL 、RocketMQ 或者 ES 中。