天天看點

從實戰出發,聊聊緩存資料庫一緻性

作者:網際網路進階架構師

在雲服務中,緩存是極其重要的一點。所謂緩存,其實是一個高速資料存儲層。當緩存存在後,日後再次請求該資料就會直接通路緩存,提升資料通路的速度。但是緩存存儲的資料通常是短暫性的,這就需要經常對緩存進行更新。而我們操作緩存和資料庫,分為讀操作和寫操作。

讀操作的詳細流程為,請求資料,如緩存中存在資料則直接讀取并傳回,如不存在則從資料庫中讀取,成功之後将資料放到緩存中。

寫操作則又分為以下 4 種:

  • 先更新緩存,再更新資料庫
  • 先更新資料庫,再更新緩存
  • 先删除緩存,再更新資料庫
  • 先更新資料庫,再删除緩存

一些一緻性要求不高的資料,如點贊數等,可以先更新緩存,然後再定時同步到資料庫。而在其它情況下,我們通常會等資料庫操作成功,再操作緩存。

下面主要介紹更新資料庫成功後,更新緩存和删除緩存這兩個操作的差別和改進方案。

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

先更新資料庫,再删除緩存,這種模式也叫 cache aside,是目前比較流行的處理緩存資料庫一緻性的方法。 它的優點是:

  • 出現資料不一緻的機率極低,實作簡單
  • 由于不更新緩存,而是删除緩存,在并發寫寫情況下,不會出現資料不一緻的情況

出現資料不一緻的情況出現在并發讀寫的場景下,詳情可見下圖:

從實戰出發,聊聊緩存資料庫一緻性

這種情況發生的機率比較低,必須要在某⼀時間區間同時存在兩個或多個寫⼊和多個讀取,是以大部分業務都容忍了這種小機率的不一緻。

雖然發生的機率較低,但還是有一些方案可以讓影響降到更低。

優化方案

第一種方案為:采用較短的過期時間來減少影響。這種方法有兩個缺點:

  • 删除後,讀請求會 miss
  • 如果緩存不一緻,不一緻的時間取決于過期時間設定

第二種方案則是采用延遲雙删的政策,比如:1分鐘以後删除緩存。這種做法也存在兩個缺點:

  • 删除緩存之前的時間裡可能會有不一緻
  • 删除後,讀請求會 miss

第三種方案為雙更新政策,思路與延遲雙删政策差不多。不同的點是,此方案不删除緩存而是更新緩存,是以讀請求就不會發生 miss。但是另一個缺點還是存在。

先更新資料庫,再更新緩存

相比先更新資料庫再删除緩存的操作,先更新資料庫再更新緩存的操作可以避免使用者請求直接打到資料庫,進而導緻緩存穿透的問題。

此方案是更新緩存,我們需要關注并發讀寫和并發寫寫兩個場景下導緻的資料不一緻。

先來看看并發讀寫的情況,步驟如下圖所示:

從實戰出發,聊聊緩存資料庫一緻性

可以看到由于 4 和 5 操作步驟都設定了緩存,如果步驟4發生在步驟5之前,那麼會出現舊值覆寫新值的情況,也就是緩存不一緻的情況。這種情況隻需要修改一下步驟5,便可解決。

優化方案

可以通過在第五步不要 set cache,改用 add cache,redis 中使用 setnx 指令來進行優化。修改後步驟示意圖如下:

從實戰出發,聊聊緩存資料庫一緻性

解決完了并發讀寫場景導緻的資料不一緻,再來看看并發寫寫情況導緻的資料不一緻問題。

出現不一緻的情況如下圖所示,Thread A 比 Thread B 先更新完 DB,但是 Thread B 卻先更新完緩存,這就導緻緩存會被 Thread A 的舊值所覆寫。

從實戰出發,聊聊緩存資料庫一緻性

這種情況也是有方法可以優化的,下面介紹兩個主流方法:

  • 使用分布式鎖
  • 使用版本号

使用分布式鎖

要解決并發讀寫的問題,第一個思路就是消滅并發寫。而使用分布式鎖,讓寫操作排隊執行,理論上就可以解決并發寫的問題,但現在并沒有可靠的分布式鎖實作方案。

從實戰出發,聊聊緩存資料庫一緻性

不管是基于 Zookeeper,etcd 還是 redis 實作分布式鎖,為了防止程式挂掉而鎖不能釋放,我們都會給鎖設定租約/過期時間,想象一種場景:如果程序卡頓幾分鐘(雖然機率較低),導緻鎖失效,而其它線程擷取到鎖,此時就又出現了并發讀寫的場景了,還是有可能會造成資料不一緻。

使用版本号

并發寫導緻的資料不一緻,是因為低版本覆寫了高版本。那麼我們可以想辦法不讓這種情況發生,一種可行的方案是引入版本号,如果寫入的資料低于現版本号,則放棄覆寫。

缺點:

  • 應用層維護版本的代價很大,大規模落地很難
  • 需修改資料模型,添加版本
  • 每次需要修改,讓版本自增

不管是更新緩存還是删除緩存,優化以後都将出現資料不一緻的機率降到最低了。但是有沒有一種辦法既簡單,又不會出現資料不一緻的場景呢。下面就介紹一下 Rockscache。

Rockscache

Rockscache 也是一種保持緩存一緻性的方法,它采用的緩存管理政策是:更新資料庫後,将緩存标記為删除。主要通過以下兩個方法來實作:

  • Fetch 函數實作了前面的查詢緩存
  • TagAsDeleted 函數實作了标記删除的邏輯

在運作時隻要讀資料時調用 Fetch,并且確定更新資料庫之後調用 TagAsDeleted,就能夠確定緩存最終一緻。這一政策有 4 個特點:

  • 不需要引入版本,幾乎可以适用于所有緩存場景
  • 架構上與"更新 DB 後删除緩存”一樣,無額外負擔
  • 性能高:變化隻是将原來的 GET/SET/DELETE,替換為 Lua 腳本
  • 強一緻方案的性能也很高,與普通的防緩存擊穿方案一樣

在 Rockscache 政策中,緩存中的資料是包含幾個字段的 hash:

  • value:資料本身
  • lockUtil:資料鎖定到期時間,當某個程序查詢緩存無資料,那麼先鎖定緩存一小段時間,然後查詢 DB,然後更新緩存
  • owner:資料鎖定者 uuid

證明

因為 Rockscache 方案并不更新緩存,是以隻要確定并發讀寫資料一緻性即可。下面來看看 Rockscache 是怎麼解決資料不一緻的問題,先回憶一遍 cache aside 模式導緻的資料不一緻的原因。

結合 cache aside 模式出現資料不一緻的場景,來講講 Rockscache 是怎麼解決的。

從實戰出發,聊聊緩存資料庫一緻性

我們要解決的核心問題是,防止舊值寫入到緩存中。Rockscache 的解決方案是這樣的:

  • 查詢請求,如果緩存中讀不到資料,還要做一個操作:鎖定緩存,為key設定一個uuid(代碼示例:github.com/dtm-labs/ro…
  • 寫請求在删除緩存的時候,需要把鎖删了(代碼示例:github.com/dtm-labs/ro…
  • 讀請求在設定緩存的時候,通過uuid比對,發現上鎖的不是自己,說明有寫請求把資料更新了,則放棄修改緩存(代碼示例:github.com/dtm-labs/ro…

至此我們已經完成了 rockscache 政策下的緩存更新。不過和其他緩存更新政策一樣,我們都預設操作資料庫成功後,操作緩存肯定成功。但是這是不對的,在實際操作過程即便操作資料庫成功,也可能出現緩存操作失敗的情況,是以可以通過以下 3 種方式來保證緩存更新成功:

  • 本地消息表
  • 監聽 binlog
  • dtm 的二階段消息

除了緩存更新,Rockscache 還有以下兩種功能:

  • 防止緩存擊穿
  • 防止防止穿透和緩存雪崩

這都是非常實用的功能,推薦大家實際使用操作試試看。

作者:又拍雲

連結:https://juejin.cn/post/7186543370712383544

來源:稀土掘金

繼續閱讀