博客 / 詳情

返回

騰訊二面:Redis與MySQL雙寫一致性如何保證?

前不久,有位朋友去騰訊面試,他説被問到 Redis 與 MySQL 的一致性如何保證? 本文將跟大家一起來探討如何回答這個問題。

為什麼要使用 Redis?

首先為了提升服務器的性能,一般都是給服務器加上 redis,讓其作為數據庫的緩存。這樣,在客户端請求數據時,如果能在緩存中命中數據,那就查詢緩存,不用再去查詢數據庫,從而減輕數據庫的壓力,提高服務器的性能。

數據更新時,先更新數據庫,還是先更新緩存?

由於引入了緩存,那麼在數據更新時,不僅要更新數據庫,而且要更新緩存,這兩個更新操作存在前後的問題:

先更新數據庫,再更新緩存;

先更新緩存,再更新數據庫;

下面讓我們來詳細介紹一下這兩種更新方式!

先更新數據庫,再更新緩存

舉個例子,比如「請求 A 」和「請求 B 」兩個請求,同時更新「同一條」數據,則可能出現這樣的順序:

簡單分析一下,A 請求先將數據庫的數據更新為 1,然後在更新緩存前,請求 B 將數據庫的數據更新為 2,緊接着也把緩存更新為 2,然後 A 請求更新緩存為 1。

此時,數據庫中的數據是 2,而緩存中的數據卻是 1,出現了緩存和數據庫中的數據不一致的現象。

數據庫的數據是客户第二次更新操作的數據,而緩存確還是第一次更新操作的數據,也就是出現了數據庫和緩存的數據不一致的問題,造成緩存和數據庫的數據不一致的現象。是因為併發問題

先更新緩存,再更新數據庫

依然還是存在併發的問題,分析思路也是一樣。

假設「請求 A 」和「請求 B 」兩個請求,同時更新「同一條」數據,則可能出現這樣的順序:

同樣的分析思路,A 請求先將緩存的數據更新為 1,然後在更新數據庫前,B 請求來了, 將緩存的數據更新為 2,緊接着把數據庫更新為 2,然後 A 請求將數據庫的數據更新為 1。

此時,數據庫中的數據是 1,而緩存中的數據卻是 2,出現了緩存和數據庫中的數據不一致的現象。

無論是「先更新數據庫,再更新緩存」,還是「先更新緩存,再更新數據庫」,這兩個方案都存在併發問題,當兩個請求併發更新同一條數據的時候,可能會出現緩存和數據庫中的數據不一致的現象。

使用 Cache Aside 策略

無論是先更新緩存,還是先更新數據庫,都會存在併發問題,導致緩存和數據庫的數據不一致。

那我們採用 Cache Aside 策略,決定在更新數據時,不更新緩存,而是刪除緩存中的數據。然後,到讀取數據時,發現緩存中沒了數據之後,再從數據庫中讀取數據,更新到緩存中。

Cache Aside 策略又可以細分為「讀策略」和「寫策略」

新的問題 —— 刪除緩存 和 更新數據庫

那麼現在又有個問題,那就是刪除緩存和更新數據庫這兩個操作,誰在前面呢?

是先更新數據庫,再刪除緩存?還是先刪除緩存,再更新數據庫?下面讓我們具體分析一下。

先刪除緩存,再更新數據庫

假設某個用户的年齡是 20,請求 A 要更新用户年齡為 21,所以它會刪除緩存中的內容。這時,另一個請求 B 要讀取這個用户的年齡,它查詢緩存發現未命中後,會從數據庫中讀取到年齡為 20,並且寫入到緩存中,然後請求 A 繼續更改數據庫,將用户的年齡更新為 21。

最終,該用户年齡在緩存中是 20(舊值),在數據庫中是 21(新值),緩存和數據庫的數據不一致。

可以看到,先刪除緩存,再更新數據庫,在「讀 + 寫」併發的時候,還是會出現緩存和數據庫的數據不一致的問題。

更新數據庫,再刪除緩存

假如某個用户數據在緩存中不存在,請求 A 讀取數據時從數據庫中查詢到年齡為 20,在未寫入緩存中時另一個請求 B 更新數據。它更新數據庫中的年齡為 21,並且清空緩存。這時請求 A 把從數據庫中讀到的年齡為 20 的數據寫入到緩存中。

最終,該用户年齡在緩存中是 20(舊值),在數據庫中是 21(新值),緩存和數據庫數據不一致。

從上面的理論上分析,先更新數據庫,再刪除緩存也是會出現數據不一致性的問題,但是在實際中,這個問題出現的概率並不高。

因為緩存的寫入通常要遠遠快於數據庫的寫入,所以在實際中很難出現請求 B 已經更新了數據庫並且刪除了緩存,請求 A 才更新完緩存的情況。

而一旦請求 A 早於請求 B 刪除緩存之前更新了緩存,那麼接下來的請求就會因為緩存不命中而從數據庫中重新讀取數據,所以不會出現這種不一致的情況。

所以,「先更新數據庫 + 再刪除緩存」的方案,是可以保證數據一致性的。

刪除緩存 和 更新數據庫 都要執行成功

但是又有了新的問題,上述成功的前提,都是建立於這兩個操作都能同時執行成功,所以問題就是,在刪除緩存(第二個操作)的時候失敗了,導致緩存中的數據是舊值,而數據庫是最新值。

如何保證兩個操作都能執行成功?

在這裏有兩種方法:

> 1. 消息隊列重試機制

消息隊列來重試緩存的刪除,優點是保證緩存一致性的問題,缺點會對業務代碼入侵
> 2. 訂閲 MySQL binlog,再操作緩存

訂閲 MySQL binlog + 消息隊列 + 重試緩存的刪除,優點是規避了代碼入侵問題,也很好的保證緩存一致性的問題,缺點就是引入的組件比較多,對團隊的運維能力比較有高要求。


這兩種方法有一個共同的特點,都是採用異步操作緩存。

消息隊列重試機制

我們可以引入消息隊列,將第二個操作(刪除緩存)要操作的數據加入到消息隊列,由消費者來操作數據。

如果應用刪除緩存失敗,可以從消息隊列中重新讀取數據,然後再次刪除緩存,這個就是重試機制。當然,如果重試超過的一定次數,還是沒有成功,我們就需要向業務層發送報錯信息了。

如果刪除緩存成功,就要把數據從消息隊列中移除,避免重複操作,否則就繼續重試。

下面是重試機制的過程:

這個方案缺點就是對代碼入侵性比較強,因為需要改造原本業務的代碼。

訂閲 MySQL binlog,再操作緩存

「先更新數據庫,再刪緩存」的策略的第一步是更新數據庫,那麼更新數據庫成功,就會產生一條變更日誌,記錄在 binlog 裏。

於是我們就可以通過訂閲 binlog 日誌,拿到具體要操作的數據,然後再執行緩存刪除,阿里的開源的 Canal 中間件就是基於這個實現的。

Canal 模擬 MySQL 主從複製的交互協議,把自己偽裝成一個 MySQL 的從節點,向 MySQL 主節點發送 dump 請求,MySQL 收到請求後,就會開始推送 Binlog 給 Canal,Canal 解析 Binlog 字節流之後,轉換為便於讀取的結構化數據,供下游程序訂閲使用。

下圖是 Canal 的工作原理:

前面我們説到直接用消息隊列重試機制方案的話,會對代碼造成入侵,那麼 Canal 方案就能很好的規避這個問題,因為它是直接訂閲 binlog 日誌的,和業務代碼沒有藕合關係,因此我們可以通過 Canal+ 消息隊列的方案來保證數據緩存的一致性。

具體的做法是:將 binlog 日誌採集發送到 MQ 隊列裏面,然後編寫一個簡單的緩存刪除消息者訂閲 binlog 日誌,根據更新 log 刪除緩存,並且通過 ACK 機制確認處理這條更新 log,保證數據緩存一致性。

這兩種方法有一個共同的特點,都是採用異步操作緩存。

就業陪跑訓練營學員投稿

歡迎關注 ❤

我們搞了一個免費的面試真題共享羣,互通有無,一起刷題進步。

沒準能讓你能刷到自己意向公司的最新面試題呢。

感興趣的朋友們可以加我微信:wangzhongyang1993,備註:思否面試羣。

user avatar redorblack 頭像 tracy_5cb7dfc1f3f67 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.