天天看點

Redis 緩存資料庫一緻性手撕面答

引言

自Redis入門篇過後,已經好久沒更Redis了,接下來應該從實戰篇,原理篇,面試篇幾個層次來展開,本篇主要是實戰篇中的資料庫緩存一緻性問題,以問題形式展開,應對面試場景作答【melo稱其為"手撕面答"】,盡量簡短,某些部分可能不會進行詳細介紹。

🎨本篇腦圖速覽

Redis 緩存資料庫一緻性手撕面答

🎯為什麼是删除緩存而不是更新緩存?

🎐懶加載

一種懶加載的思想,因為每次更改資料之後,不一定立馬就有人來用。 若更新的次數遠大于讀取的次數,此時會頻繁更新緩存,但一直沒人使用,若緩存更新的成本很高的話,此時會非常浪費性能資源。

🎈并發更新的情況

ABBA 【A的操作過程中,穿插了B的完整過程】

  1. A更新資料庫為1
  2. B更新資料庫為2
  3. B更新緩存為2
  4. A更新緩存為1
最後導緻資料庫最終是2,但緩存是1,也就是B的緩存更新丢失了
Redis 緩存資料庫一緻性手撕面答

🎯為什麼要先更新資料庫,再删除緩存

若先删除緩存,再更新資料庫的話:

  1. A線程删除緩存
  2. B線程查詢資料,緩存中沒有了,會去計算資料庫,設定緩存【此時計算出來的緩存是舊資料,是資料庫更新之前的資料】
  3. A線程再更新資料庫

接下來的查詢,一直走的是緩存,也就是舊資料,這樣就出現了資料庫和緩存中資料不一緻的情況

若先更新資料庫,再删除緩存的話

  1. A線程更新資料庫
  2. B線程查詢資料,此時還未删除緩存,緩存中還有,得到的就是舊的緩存資料【此時就出現了資料庫和緩存中資料不一緻的情況】
  3. A線程再删除緩存

A讀B寫【先更後删可能會有資料不一緻的情況,但很少見】

  1. A讀取,發現緩存剛好過期了
  2. A查詢出資料庫的舊值,在設定緩存之前
  3. B更新資料庫,并删除了緩存
  4. A用舊值,設定了緩存【緩存中是舊的資料,資料庫是新的】

但由于緩存的寫入要快于MySQL的寫入,一般不可能在2跟4之間,穿插一個3操作【3還操作了資料庫】

  • 如果順序是【2,4,3】的話,A設定了緩存之後也沒關系,因為後續B還會再次删除緩存
  • 之後的查詢,發現緩存過期會去資料庫中查詢得到最新的資料

🎯🎈如何解決A讀B寫時先更後删的極端情況?

延時雙删

  1. A讀取,發現緩存失效了,此時去查詢資料庫【查出舊值】
  2. B更新資料庫
  3. B設定新緩存
  4. A設定緩存為舊值
到這一步,如果之後的查詢,查到的都會是舊的緩存,是以我們可以
  1. 延時500毫秒
延時500毫秒是為了讓B能夠更新完資料庫,我們再次查詢資料庫才能獲得最新的值,來設定緩存
  1. 删除緩存

🎯先删除緩存,後更新資料庫如果也出現類似上邊的問題怎麼辦?

假設一個場景:

  • 請求A進行寫操作,删除緩存
  • 請求B查詢發現緩存不存在
  • 請求B去資料庫查詢得到舊值
  • 請求B将舊值寫入緩存
  • 請求A将新值寫入資料庫

這時候緩存中的資料就是髒資料,如果緩存沒有設定過期時間的話,就導緻這個資料在下一次修改之前傳回給使用者的都是髒資料。

Redis 緩存資料庫一緻性手撕面答

🎯從後者操作可能失敗的角度來看,選擇哪種政策更好一點?

先更新後删除緩存的話,後者失敗,情況是這樣的:

  1. A更新資料庫,然後緩存沒有删除成功
  2. B查詢,直接走了緩存【舊資料】
這就出現了緩存跟資料庫不一緻的情況

先删除緩存後更新資料庫的話,後者失敗,情況是這樣的:

  1. A删除緩存,然後沒能更新資料庫
  2. B查詢,發現緩存沒有,則去查詢資料庫,設定到緩存裡邊
此時并不會出現緩存跟資料庫不一緻的情況,因為A還沒來得及更新,這樣資料庫跟緩存中,一直都是舊的資料,但至少不會出現資料不一緻的問題
  • 是以如果從這個角度來看的話,先删除緩存,後更新資料庫才是更優解。

🎯🎈如何解決後者失敗的情況呢?

使用消息隊列重試

  1. 請求 A 先對資料庫進行更新操作,同時吧
  2. 在對 Redis 進行删除操作的時候發現報錯,删除失敗
  3. 此時将Redis 的 key 作為消息體發送到消息隊列中
  4. 系統接收到消息隊列發送的消息後再次對 Redis 進行删除操作
Redis 緩存資料庫一緻性手撕面答

但是這個方案會有一個缺點就是會對業務代碼造成大量的侵入,深深的耦合在一起。

有沒有必要更新成功後就投遞到mq呢?而不是等到redis删除失敗了再投遞到mq

如果考慮Java項目程序在更新資料庫之後就當機了的話,那無論哪種都沒法避免緩存删除的失敗 如果硬摳的話,更新成功後,到删除redis還有一段間隙,這個間隙先投遞到mq,會更有保障一點,但相應的編碼規則也更麻煩了點

  1. 更新資料庫成功,投遞到mq表示要删除 某個key
  2. 業務繼續執行,删除redis:
  1. 删除成功的話,需要删除掉mq裡邊的消息,防止重複消費
  2. 删除失敗的話,就需要消費者拉取mq,再次删除緩存,實作重試機制,當然,如果重試超過的一定次數,還是沒有成功,我們就需要向業務層發送報錯資訊了。後續的處理可能就先丢到死信隊列裡邊了。
可以發現編碼确實麻煩了些,因為要考慮重複消費的問題。那麼如何解決呢?Java崩了,咱下遊還有 mysql 和 mq 沒崩呢!也就是以引出了下文的訂閱binlog日志的方法

訂閱binlog日志

原理:更新資料庫成功,就會産生一條變更日志,記錄在 binlog 裡。

于是我們就可以通過訂閱 binlog 日志,拿到具體要操作的資料,然後再執行緩存删除,阿裡巴巴開源的 Canal 中間件就是基于這個實作的。

具體流程:

  1. canal模拟mysql slave的互動協定,僞裝自己為mysql slave,向mysql master發送dump協定;
  2. mysql master收到dump請求,開始推送binary log給slave(也就是canal);
  3. canal解析binary log對象(原始為byte流);
  4. canal将解析後的對象,根據業務場景,分發到比如 MySQL 、RocketMQ 或者 ES 中。

總結