5.2.2 先更新DB,再删除Cache
這種情況下,如果其他線程并發讀緩存的請求不多,那麼,就不會有很多請求讀取到舊值。
而且,線程A一般也會很快删除緩存值,這樣一來,其他線程再次讀取時,就會發生緩存缺失,進而從資料庫中讀取最新值。是以,這種情況對業務的影響較小。
至此,Cache和DB資料不一緻的原因也都有了對應解決方案。
删除Cache或更新DB失敗而導緻資料不一緻
重試,確定删除或更新成功
在删除Cache、更新DB這兩步操作中,有其他線程的并發讀操作,導緻其他線程讀取到舊值
延遲雙删
絕大多數場景都會将Redis作為隻讀緩存:
既可以先删除緩存值再更新資料庫
也可以先更新資料庫再删除緩存
推薦優先使用先更新資料庫再删除緩存:
先删除緩存值再更新資料庫,有可能導緻請求因緩存缺失而通路資料庫,給資料庫帶來壓力
如果業務應用中讀取資料庫和寫緩存的時間不好估算,那麼,延遲雙删中的等待時間就不好設定
不過,當使用先更新資料庫再删除緩存時,也有個地方需要注意,如果業務層要求必須讀取一緻的資料,那麼,我們就需要在更新資料庫時,先在Redis緩存用戶端暫存并發讀請求,等資料庫更新完、緩存值删除後,再讀取資料,進而保證資料一緻性。
6 直接更新 Cache
在隻讀緩存中進行資料的删改操作時,需要在緩存中删除相應的緩存值。若此過程不是删除緩存,而是直接更新緩存,效果如何?
這種情況相當于把Redis當做讀寫緩存使用,删改操作同時操作DB、Cache。
6.1 無并發
先更新資料庫,再更新緩存
若更新DB成功,但Cache更新失敗,此時DB最新值,但緩存舊值,後續讀請求會直接命中緩存,得到舊值。
先更新緩存,再更新資料庫
如果更新緩存成功,但資料庫更新失敗:
- 緩存中是最新值
- 資料庫中是舊值
後續讀請求會直接命中緩存,但得到的是最新值,短期對業務影響不大。但一旦緩存過期或滿容後被淘汰,讀請求就會從資料庫中重新加載舊值到緩存中,之後的讀請求會從緩存中得到舊值,對業務産生影響。
針對這種其中一個操作可能失敗的情況,類似隻讀緩存方案,也可使用重試。把第二步操作放入到MQ中,消費者從MQ取出消息,再更新緩存或資料庫,成功後把消息從消息隊列删除,否則進行重試,以此達到資料庫和緩存的最終一緻。
6.2 并發讀寫
也會産生不一緻,分為以下4種雙寫場景。
雙寫模式下,更新DB有傳回值,更新Redis的操作可放到更新DB傳回後進行,通過資料庫的行鎖機制,可以避免更新DB是線程A,B,但更新Redis是線程B,A的情況。
寫+讀并發。
線程A先更新資料庫,之後線程B讀取資料,此時線程B會命中緩存,讀取到舊值,之後線程A更新緩存成功,後續的讀請求會命中緩存得到最新值。
這時,線程A未更新完緩存之前,在這期間的讀請求會短暫讀到舊值,對業務短暫影響。
線程A先更新緩存成功,之後線程B讀取資料,此時線程B命中緩存,讀取到最新值後傳回,之後線程A更新資料庫成功。這種場景下,雖然線程A還未更新完資料庫,資料庫會與緩存存在短暫不一緻,但在這之前進來的讀請求都能直接命中緩存,擷取到最新值,是以對業務沒影響。
寫+寫并發。
線程A和線程B同時更新同一條資料,更新資料庫的順序是先A後B,但更新緩存時順序是先B後A,這會導緻資料庫和緩存的不一緻。
與場景3類似,線程A和線程B同時更新同一條資料,更新緩存的順序是先A後B,但是更新資料庫的順序是先B後A,這也會導緻資料庫和緩存的不一緻。
場景1和2對業務影響較小,場景3和4會造成資料庫和緩存不一緻,影響較大。即讀寫緩存下,寫+讀并發對業務的影響較小,而寫+寫并發時,會造成資料庫和緩存的不一緻。
針對場景3、4解決方案:對于寫請求,配合分布式鎖。寫請求進來時,針對同一資源的修改操作,先加分布式鎖,這樣同一時間隻允許一個線程去更新DB和Cache,沒有拿到鎖的線程把操作放入到MQ,延時處理。
這樣保證多個線程操作同一資源的順序性,以此保證一緻性。
綜上,使用讀寫緩存同時操作資料庫和緩存時,因為其中一個操作失敗導緻不一緻的問題,同樣可以通過MQ重試解決。
而在并發的場景下,讀+寫并發對業務沒有影響或者影響較小,而寫+寫并發時需要配合分布式鎖的使用,才能保證緩存和資料庫的一緻性。
另外,讀寫緩存模式由于會同時更新資料庫和緩存:
優點
緩存一直會有資料。若更新後立即通路,可直接命中緩存,能降低讀請求對DB的壓力(沒有隻讀緩存的删除緩存導緻緩存缺失和再加載的過程)
缺點
若更新後的資料,之後很少再被通路到,會導緻緩存中保留的不是最熱資料,緩存使用率不高(隻讀緩存中保留的都是熱資料)
是以讀寫緩存比較适合用于讀寫相當的業務場景。
總結
延時雙删政策
寫DB前後都執行
redis.del(key)
,并設定合理逾時時間。
執行流程
- 先删除緩存
- 再寫資料庫
- 休眠xx毫秒(根據具體業務時間)
- 再次删除緩存
xx毫秒怎麼确定?
需要評估項目讀資料業務邏輯耗時,以確定讀請求結束,寫請求可删除讀請求造成的緩存髒資料。
該政策還要考慮 redis 和資料庫主從同步的耗時。最後的寫資料的休眠時間:則在讀資料業務邏輯的耗時的基礎上,加上幾百ms即可。比如:休眠1秒。
設定緩存過期時間
理論上,設定緩存過期時間,是保證最終一緻性的解決方案。
所有的寫操作以DB為準,隻要到達緩存過期時間,則後面的讀請求自然會從DB讀取新值,然後回填緩存。
結合雙删政策+緩存逾時設定,這樣最差的情況就是在逾時時間内資料存在不一緻,而且又增加寫請求耗時。
寫完資料庫後,再次删除緩存成功保證
上述的方案有一個缺點,那就是操作完資料庫後,由于種種原因删除緩存失敗,這時,可能就會出現資料不一緻的情況。
需提供保障重試方案。
方案一
具體流程
- 更新資料庫資料
- 緩存因為種種問題删除失敗
- 将需要删除的key發送至消息隊列
- 自己消費消息,獲得需要删除的key
- 繼續重試删除操作,直到成功
然而,該方案有一個缺點,對業務線代碼造成大量的侵入。于是有了方案二。
在方案二中,啟動一個訂閱程式去訂閱資料庫的binlog,獲得需要操作的資料。在應用程式中,另起一段程式,獲得這個訂閱程式傳來的資訊,進行删除緩存操作。
方案二
- 資料庫會将操作資訊寫入binlog日志當中
- 訂閱程式提取出所需要的資料以及key
- 另起一段非業務代碼,獲得該資訊
- 嘗試删除緩存操作,發現删除失敗
- 将這些資訊發送至消息隊列
- 重新從消息隊列中獲得該資料,重試操作。
以上方案都是在業務中經常會碰到的場景,可以依據業務場景的複雜和對資料一緻性的要求來選擇具體的方案。