天天看點

Redis資料一緻性問題的三種解決方案

作者:一個即将退役的碼農
Redis資料一緻性問題的三種解決方案

1、首先redis是什麼

Redis(Remote Dictionary Server ),是一個高性能的基于Key-Value結構存儲的NoSQL開源資料庫。大部分公司采用Redis來實作分布式緩存,用來提高資料查詢效率。

2、為什麼會選Redis

在Web應用發展的初期,系統的通路和并發并不高,互動也比較少。但随着業務的擴大,通路量的提升,使得伺服器負載和關系型資料庫出現瓶頸,而導緻瓶頸的源頭,主要展現在磁盤IO上。随着網際網路的進一步發展,對系統性能有了更高的要求,Redis的出現,解決了很多問題。至于我們為什麼要選擇Redis,我總結為以下六個原因:

1)、基于記憶體存儲,可以降低對關系型資料庫的通路頻次,進而緩解資料庫壓力

2)、資料IO操作能支援更進階别的QPS,官方釋出的名額是10W;

3)、提供了比較多的資料存儲結構,比如string、list、hash、set、zset等等。

4)、采用單線程實作IO操作,避免了并發情況下的線程安全問題。

5)、可以支援資料持久化,避免因伺服器故障導緻資料丢失的問題

6)、Redis還提供了更多進階功能,比如分布式鎖、分布式隊列、排行榜、查找附近的人等功能,為更複雜的需求提供了成熟的解決方案。

3、 應用場景

緩存,作為Key-Value形态的記憶體資料庫,Redis 最先會被想到的應用場景便是作為資料緩存

分布式鎖,分布式環境下對資源加鎖

分布式共享資料,在多個應用之間共享

排行榜,自帶排序的資料結構(zset)

消息隊列,pub/sub功能也可以用作釋出者 / 訂閱者模型的消息

Redis資料一緻性問題的三種解決方案

4、redis用作緩存時

4.1、作為緩存使用流程

緩存由于其高并發和高性能的特性,已經在項目中被廣泛使用。在讀取緩存方面,大家沒啥疑問,都是按照下圖的流程來進行業務操作。

Redis資料一緻性問題的三種解決方案

4.2、資料性一緻性問題

例如我們使用Redis來作為緩存時,讓請求先通路到Redis,而不是直接通路資料庫。而在這種業務場景下,可能會出現緩存和資料庫資料不一緻性的問題。

在更新的時候,操作緩存和資料庫無疑就是以下四種可能之一:

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

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

Redis資料一緻性問題的三種解決方案

如果我成功更新了緩存,但是在執行更新資料庫的那一步,伺服器突然當機了,那麼此時,我的緩存中是最新的資料,而資料庫中是舊的資料。

髒資料就是以誕生了,并且如果我緩存的資訊(是單獨某張表的),而且這張表也在其他表的關聯查詢中,那麼其他表關聯查詢出來的資料也是髒資料,結果就是直接會産生一系列的問題。

4.2.2、先更新資料庫,在更新緩存

Redis資料一緻性問題的三種解決方案

隻有等到緩存過期之後,才能通路到正确的資訊。那麼在緩存沒過期的時間段内,所看到的都是髒資料。

以上兩圖中隻要執行第二步時失敗了,就必然會産生髒資料。

4.2.3、先删除緩存,在更新資料庫

這種方式在沒有高并發的情況下,是可能保持資料一緻性的。

Redis資料一緻性問題的三種解決方案

如果隻有第一步執行成功,而第二步失敗,那麼隻有緩存中的資料被删除了,但是資料庫沒有更新,那麼在下一次進行查詢的時候,查不到緩存,隻能重新查詢資料庫,建構緩存,這樣其實也是相對做到了資料一緻性。

但如果是處于讀寫并發的情況下,還是會出現資料不一緻的情況:

Redis資料一緻性問題的三種解決方案

執行完成後,明顯可以看出,1号使用者所建構的緩存,并不是最新的資料,還是存在問題的

4.2.4、先更新資料庫,在删除緩存

如果更新資料庫成功了,而删除緩存失敗了,那麼資料庫中就會是新資料,而緩存中是舊資料,資料就出現了不一緻情況。

Redis資料一緻性問題的三種解決方案

和之前一樣,如果兩段代碼都執行成功,在并發情況下會是什麼樣呢?

Redis資料一緻性問題的三種解決方案

還是會造成資料的不一緻性。

但是此處達成這個資料不一緻性的條件明顯會比起其他的方式更為困難 :

  • 時刻1:讀請求的時候,緩存正好過期
  • 時刻2:讀請求在寫請求更新資料庫之前查詢資料庫,
  • 時刻3:寫請求,在更新資料庫之後,要在讀請求成功寫入緩存前,先執行删除緩存操作。

這通常是很難做到的,因為在真正的并發開發中,更新資料庫是需要加鎖的,不然沒一點安全性~

一定程度上來講,這種方式還是解決了一定程度上的資料不一緻性問題的。

4.3、總結

以上四種方式無論選擇那種方式,如果實在多服務或時并發的情況下,其實都是有可能産生資料不一緻性的。

為了解決這個存在的問題有以下方式:

4.3.1、延遲雙删

先進行緩存清除,再執行update,最後(延遲N秒)再執行緩存清除。進行兩次删除,且中間需要延遲一段時間

Redis資料一緻性問題的三種解決方案
public void write(String key,Object data){
// 延遲雙删僞代碼
		deleteRedisCache(key);   // 删除redis緩存
		updateMysqlSql(obj);        // 更新mysql
		Thread.sleep(100);           // 延遲一段時間
		deleteRedisCache(key);   // 再次删除該key的緩存
}
           

延遲雙删的流程圖:

Redis資料一緻性問題的三種解決方案

解決這樣的問題,其實最好的方式就是在執行完更新資料庫的操作後,先休眠一會兒,再進行一次緩存的删除,以確定資料一緻性

首先延遲删除的時間需要大于 1号使用者執行流程的總時間

就是1号使用者從資料庫讀取資料 寫入緩存時間

4.3.2、通過發送MQ,在消費者線程去同步Redis

Redis資料一緻性問題的三種解決方案

無論是更新緩存還是删除緩存,在同時操作緩存和資料庫時,都無法保證兩者都能一次性操作成功,是以我們最好的辦法就是重試,這個重試并不是立即重試,因為緩存和資料庫可能因為網絡或者其它原因停止服務了,立即重試成功率極低,而且重試會占用線程資源,顯然不合理,是以我們需要采用異步重試機制。

異步重試我們可以使用消息隊列來完成,因為消息隊列可以保證消息的可靠性,消息不會丢失,也可以保證正确消費,當且僅當消息消費成功後才會将消息從消息隊列中删除。

優點1:可以大幅減少接口的延遲傳回的問題

優點2:MQ本身有重試機制,無需人工去寫重試代碼

優點3:解耦,把查詢Mysql和同步Redis完全分離,互不幹擾

4.3.3、Canal 訂閱日志實作

當我們業務修改資料時,我們隻需要更新資料庫,無需修改緩存,那什麼時候修改緩存呢?

以mysql為例,在資料庫一條記錄發生變更時就會生成一條binlog日志,我們可以訂閱這種消息,拿到具體的資料,然後根據日志消息更新緩存,訂閱日志目前比較流行的就是阿裡開源的canal,那麼我們的架構就變為如下形式。

Redis資料一緻性問題的三種解決方案

訂閱資料庫變更日志,當資料庫發生變更時,我們可以拿到具體操作的資料,然後再去根據具體的資料,去删除對應的緩存。

當然Canal 也是要配合消息隊列一起來使用的,因為其Canal本身是沒有資料處理能力的。

Redis資料一緻性問題的三種解決方案

這個方式算的上徹底解耦了,應用程式代碼無需再管消息隊列方面發送失敗問題,全交由 Canal來發送。

繼續閱讀