如何解決Redis緩存和MySQL數據一致性的問題?

1029417316


在高併發的業務場景下,數據庫的性能瓶頸往往都是用戶併發訪問過大。所以,一般都使用redis做一個緩衝操作,讓請求先訪問到redis,而不是直接去訪問MySQL等數據庫。從而減少網絡請求的延遲響應

數據為什麼會不一致

這樣的問題主要是在併發讀寫訪問的時候,緩存和數據相互交叉執行。

一、單庫情況下

同一時刻發生了併發讀寫請求,例如為A(寫) B (讀)2個請求

  1. A請求發送一個寫操作到服務端,第一步會淘汰cache,然後因為各種原因卡主了,不在執行後面業務(例:大量的業務操作、調用其他服務處理消耗了1s)。

  2. B請求發送一個讀操作,讀cache,因為cache淘汰,所以為空

  3. B請求繼續讀DB,讀出一個髒數據,並寫入cache

  4. A請求終於執行完全,在寫入數據到DB

    總結:因最後才把寫操作數據入DB,並沒同步。cache裡面一直保持髒數據

    髒數據是指源系統中的數據不在給定的範圍內或對於實際業務毫無意義,或是數據格式非法,以及在源系統中存在不規範的編碼和含糊的業務邏輯。

二、主從同步,讀寫分離的情況下,讀從庫而產生髒數據

  1. A請求發送一個寫操作到服務端,第一步會淘汰cache

  2. A請求寫主數據庫,寫了最新的數據。

  3. B請求發送一個讀操作,讀cache,因為cache淘汰,所以為空

  4. B請求繼續讀DB,讀的是從庫,此時主從同步還沒同步成功。讀出髒數據,然後髒數據入cache

  5. 最後數據庫主從同步完成

    總結:這種情況下請求A和請求B操作時序沒問題,是主從同步的時延問題(假設1s),導致讀請求讀取從庫讀到髒數據導致的不一致

根本原因:

單庫下,邏輯處理中消耗1s。可能讀到舊數據入緩存

主從+讀寫分離,在1s的主從同步時延中。讀到從庫的舊數據入緩存


數據優化方案

一、緩存雙淘汰法

  1. 先淘汰緩存

  2. 再寫數據庫

  3. 往消息總線esb發送一個淘汰消息,發送立即返回。寫請求的處理時間幾乎沒有增加,這個方法淘汰了緩存兩次。因此被稱為“緩存雙淘汰法“,而在消息總線下游,有一個異步淘汰緩存的消費者,在拿到淘汰消息在1s後淘汰緩存,這樣,即使在一秒內有髒數據入緩存,也能夠被淘汰掉。

二、異步淘汰緩存

上述的步驟,都是在業務線裡面執行,新增一個線下的讀取binlog異步淘汰緩存模塊,讀取binlog總的數據,然後進行異步淘汰。

1.思路:

MySQL binlog增量發佈訂閱消費+消息隊列+增量數據更新到redis

1)讀請求走Redis:熱數據基本都在Redis

2)寫請求走MySQL: 增刪改都操作MySQL

3)更新Redis數據:MySQ的數據操作binlog,來更新到Redis

2.Redis更新

1)數據操作主要分為兩塊:

  • 一個是全量(將全部數據一次寫入到redis)

  • 一個是增量(實時更新)

這裡說的是增量,指的是mysql的update、insert、delate變更數據。

這樣一旦MySQL中產生了新的寫入、更新、刪除等操作,就可以把binlog相關的消息推送至Redis,Redis再根據binlog中的記錄,對Redis進行更新。就無需在從業務線去操作緩存內容



PHP智慧與能力


Redis緩存和MySQL數據一致性的問題,相信大家在大量使用redis緩存進行業務開發的場景下是一定要考慮的問題。

總有運營、產品、測試人員過來問你為什麼我剛剛更新了一條數據,APP上還是原來的數據呢?你總是一句話:“加了緩存,等會兒就好”。有可能是1分鐘,有可能是半小時。問你的都屬於關係不錯,不問你直接給你提bug你也沒辦法。

下面就分享一下我個人在工作中對如何解決redis緩存和MySQL數據一致性的一些心得:

簡單粗暴

大家看了這個圖是不是就知道什麼意思了?一個請求過來查詢數據,我先看看redis有沒有,有直接返回,沒有就去數據庫查出來,順便同步到redis,設置一下過期時間。下次同樣的數據查詢redis緩存就可以直接返回了。是不是很簡單很粗暴?在實時性要求不高的場景下,這種方式我估計是大家最常用的一種方式。但是他有幾個問題:

  1. 無法保證一致性:數據庫更改了數據,redis裡的數據就和數據庫不一致了,產生髒讀;

  2. 緩存雪崩:這種方案要求redis裡的緩存必須設置有效期,如果在同一時間大面積過期,所有請求壓力都指向數據庫,這個時候數據庫頂不住壓力就會宕機,然後整個世界都安靜了;

  3. 緩存穿透:查詢一個數據,緩存沒有,去查詢數據庫,數據庫也沒有,怎麼辦?這樣的請求多了對數據庫也是壓力,沒有數據的時候也需要在redis緩存一個空值。

緩存同步

針對第一種方案的問題,那麼大多數時候我們會繼續做一個job,去定時同步數據庫裡的數據都redis緩存中,我們的業務請求直接查詢redis緩存,無論有或者沒有數據都直接返回結果。這樣可以避免緩存穿透、緩存雪崩等問題,也能緩解redis緩存和數據庫不一致的情況,但是還無法徹底解決一致性的問題。在job的間隔期內對數據的修改必須要等到下一次job的運行。

緩存同步加強版

針對上面的2中方案的問題,我們又搞了第三種方案,簡單概括就是:“實時刷新、定時同步”八字方案。具體如下:

  1. 增加一個cache中間件,專門處理各類緩存同步事件,採用消息隊列機制保證每一次數據的操作都能夠刷新對應的緩存數據;

  2. 採用elastic-job做定時的增量/全量數據同步;

  3. 服務請求大部分走redis,高併發API只走緩存,不允許流量進入數據庫層面;

以上三種方案我們都有采用,即使現在更多采用第三種方案,但是前兩種我們依舊在用,不同的場景採用不同的方案。三種方案的結合基本上能滿足我們的業務在數據一致性的需求。

大家覺得第三種方案在解決redis緩存和MySQL數據一致性方面有沒有什麼問題?還有其他更好的辦法嗎?歡迎評論區交流討論~


java架構設計


對軟件開發同學來說,這個場景太常見了。基本的思路是:mysql數據發生變更的時候,要及時清除redis緩存。那究竟要怎樣清除呢?

分兩種情況來說吧。

單集群。在redis集群和mysql集群都是單集群部署的情況下,需要確保如果redis查不到數據讀取mysql主庫來填充數據,此時只需要在mysql變更生效後,直接調用redis清緩存即可。

多集群。在redis集群和mysql集群都是多地多機房的部署情況下。採用單集群清緩存策略清除本機房redis集群緩存後,延遲消息通知其他機房redis集群(確保其他機房mysql集群已收到本機房mysql集群的binlog變更消息並將變更生效)。

希望能解答您的問題,喜歡的話可以關注下我哈


分享到:


相關文章: