首頁 > 軟體

Java中如何保證快取一致性問題

2022-04-21 16:01:16

前言:

一道之前的面試題:

如何保證快取和資料庫的一致性?

下面介紹幾種方案(大家回答的時候最好根據自己的業務,結合下面的方案)

方案分析

更新快取策略方式常見的有下面幾種:

  • 先更新快取,再更新資料庫
  • 先更新資料庫,再更新快取
  • 先刪除快取,再更新資料庫
  • 先更新資料庫,再刪除快取

下面一一介紹!

方案一:更新快取,更新資料庫

這種方式可輕易排除,因為如果先更新快取成功,但是資料庫更新失敗,則肯定會造成資料不一致。

方案二:更新資料庫,更新快取

這種快取更新策略俗稱雙寫,存在問題是:並行更新資料庫場景下,會將髒資料刷到快取

updateDB();
updateRedis();

舉例:如果在兩個操作之間資料庫和快取又被後面請求修改,此時再去更新快取已經是過期資料了。

方案三:刪除快取,更新資料庫

存在問題:更新資料庫之前,若有查詢請求,會將髒資料刷到快取

deleteRedis();
updateDB();

舉例:如果在兩個操作之間發生了資料查詢,那麼會有舊資料放入快取。

該方案會導致請求資料不一致

如果同時有一個請求A進行更新操作,另一個請求B進行查詢操作。那麼會出現如下情形:

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

上述情況就會導致不一致的情形出現。而且,如果不採用給快取設定過期時間策略,該資料永遠都是髒資料。

方案四:更新資料庫,刪除快取

存在問題:在更新資料庫之前有查詢請求,並且快取失效了,會查詢資料庫,然後更新快取。如果在查詢資料庫和更新快取之間進行了資料庫更新的操作,那麼就會把髒資料刷到快取

updateDB();
deleteRedis();

舉例:如果在查詢資料庫和放入快取這兩個操作中間發生了資料更新並且刪除快取,那麼會有舊資料放入快取。

假設有兩個請求,一個請求A做查詢操作,一個請求B做更新操作,那麼會有如下情形產生

  • 快取剛好失效
  • 請求A查詢資料庫,得一箇舊值
  • 請求B將新值寫入資料庫
  • 請求B刪除快取
  • 請求A將查到的舊值寫入快取

如果發生上述情況,確實是會發生髒資料。但是發生上述情況有一個先天性條件,就是寫資料庫操作比讀資料庫操作耗時更短

不過資料庫的讀操作的速度遠快於寫操作的

因此這一情形很難出現。

方案對比

方案1和方案2的共同缺點:

並行更新資料庫場景下,會將髒資料刷到快取,但一般並行寫的場景概率都相對小一些;

執行緒安全形度,會產生髒資料,比如:

  • 執行緒A更新了資料庫
  • 執行緒B更新了資料庫
  • 執行緒B更新了快取
  • 執行緒A更新了快取

方案3和方案4的共同缺點:

不管採用哪種順序,2種方式都是存在一些問題的:

  • 主從延時問題:不管是先刪除還是後刪除,資料庫主從延時可能導致髒資料的產生。
  • 快取刪除失敗:如果快取刪除失敗,則都會產生髒資料。

問題解決思路:延遲雙刪,新增重試機制,下面介紹!

更新快取還是刪除快取?

  • 1.更新快取快取需要有一定的維護成本,而且會存在並行更新的問題
  • 2.寫多讀少的情況下,讀請求還沒有來,快取以及被更新很多次,沒有起到快取的作用
  • 3.放入快取的值可能是經過複雜計算的,如果每次更新,都計算寫入快取的值,浪費效能的

刪除快取優點:簡單、成本低,容易開發;缺點:會造成一次cache miss

如果更新快取開銷較小並且讀多寫少,基本不會有寫並行的時候可以才用更新快取,否則通用做法還是刪除快取。

總結

方案問題問題出現概率推薦程度
更新快取 -> 更新資料庫為了保證資料準確性,資料必須以資料庫更新結果為準,所以該方案絕不可行不推薦
更新資料庫 -> 更新快取並行更新資料庫場景下,會將髒資料刷到快取並行寫場景,概率一般寫請求較多時會出現不一致問題,不推薦使用。
刪除快取 -> 更新資料庫更新資料庫之前,若有查詢請求,會將髒資料刷到快取並行讀場景,概率較大讀請求較多時會出現不一致問題,不推薦使用
更新資料庫 -> 刪除快取在更新資料庫之前有查詢請求,並且快取失效了,會查詢資料庫,然後更新快取。如果在查詢資料庫和更新快取之間進行了資料庫更新的操作,那麼就會把髒資料刷到快取並行讀場景&讀操作慢於寫操作,概率最小讀操作比寫操作更慢的情況較少,相比於其他方式出錯的概率小一些。勉強推薦。

推薦方案

延遲雙刪

採用更新前後雙刪除快取策略

public void write(String key,Object data){
  redis.del(key);
     db.update(data);
     Thread.sleep(1000);
     redis.del(key);
 }
  • 先淘汰快取
  • 再寫資料庫
  • 休眠1秒,再次淘汰快取

大家應該評估自己的專案的讀資料業務邏輯的耗時。然後寫資料的休眠時間則在讀資料業務邏輯的耗時基礎上即可。

這麼做的目的,就是確保讀請求結束,寫請求可以刪除讀請求造成的快取髒資料。

問題及解法:

1、同步刪除,吞吐量降低如何處理

將第二次刪除作為非同步的,提交一個延遲的執行任務

2、解決刪除失敗的方式:

新增重試機制,例如:將刪除失敗的key,寫入訊息佇列;但對業務耦合有些嚴重;

延時工具可以選擇:

最普通的阻塞Thread.currentThread().sleep(1000);

Jdk排程執行緒池,quartz定時任務,利用jdk自帶的delayQueue,netty的HashWheelTimer,Rabbitmq的延時佇列,等等

實際場景

我們有個商品中心的場景,是讀多寫少的服務,並且寫資料會傳送MQ通知下游拿資料,這樣就需要嚴格保證快取和資料庫的一致性,需要提供高可靠的系統服務能力。

寫快取策略

  • 快取key設定失效時間
  • 先DB操作,再快取失效
  • 寫操作都標記key(美團中介軟體)強制走主庫
  • 接入美團中介軟體監聽binlog(美團中介軟體)變化的資料在進行兜底,再刪除快取

讀快取策略

  • 先判斷是否走主庫
  • 如果走主庫,則使用標記(美團中介軟體)查主庫
  • 如果不是,則檢視快取中是否有資料
  • 快取中有資料,則使用快取資料作為結果
  • 如果沒有,則查DB資料,再寫資料到快取

注意

關於快取過期時間的問題

如果快取設定了過期時間,那麼上述的所有不一致情況都只是暫時的。

但是如果沒有設定過期時間,那麼不一致問題就只能等到下次更新資料時解決。

所以一定要設定快取過期時間

到此這篇關於Java快取一致性問題的文章就介紹到這了,更多相關Java快取一致性內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com