老司機帶你玩轉面試(6):分散式鎖、併發競爭、雙寫一致

極客挖掘機發表於2020-07-19

前文回顧

建議前面文章沒看過的同學先看下前面的文章:

「老司機帶你玩轉面試(1):快取中介軟體 Redis 基礎知識以及資料持久化」

「老司機帶你玩轉面試(2):Redis 過期策略以及快取雪崩、擊穿、穿透」

「老司機帶你玩轉面試(3):Redis 高可用之主從模式」

「老司機帶你玩轉面試(4):Redis 高可用之哨兵模式」

「老司機帶你玩轉面試(5):Redis 叢集模式 Redis Cluster」

併發競爭

這個問題產生的根源是併發寫,本身 Redis 是不會產生併發問題的,看過前面文章的同學應該知道, Redis 的執行緒模型是單執行緒的,所有的操作指令都在檔案事件處理器的佇列中,這個絕對是按照先進先出的原則進行操作的,那麼併發競爭的問題是如何產生的?

例如我們現在有三個客戶端 A , B , C ,需要按序像 Redis 中寫入或者更新資料 A -> B -> C ,就像下面這樣:

但是現在突然 A 的網路抖動了一下,導致 A 並不是第一個去 Redis 中寫資料的,整個流程的操作順序變成了 B -> C -> A ,這樣資料不就錯了麼。

這就好比你在網上買東西,加購物車,下訂單,支付訂單的順序變掉了,你先下單,再支付,再加購物車,這個流程根本走不下去的好吧。這種事情發生線上上的系統上的時候,是非常恐怖的。

這種情況解決起來其實也很簡單,加一個分散式鎖就可以了,比如這樣:

我們可以基於 Zookeeper 實現一個全域性分散式鎖,確保同一時間,只能有一個系統例項在操作某個 key ,別人都不允許讀和寫。

你要寫入快取的資料,都是從 DB 裡查出來的,都得寫入 DB 中,寫入 DB 中的時候必須儲存一個時間戳,從 DB 查出來的時候,時間戳也查出來。

每次要寫之前,先判斷一下當前這個 Value 的時間戳是否比快取裡的 Value 的時間戳要新。如果是的話,那麼可以寫,否則,就不能用舊的資料覆蓋新的資料。

雙寫一致性

只要使用快取,就可能會涉及到快取與資料庫的雙寫,只要是雙寫,就一定會有資料一致性的問題。

首先先介紹下經典的 Redis + BD 的讀寫模式:

  • 讀的時候,先讀快取,快取沒有的話,就讀資料庫,然後取出資料後放入快取,同時返回響應。
  • 更新的時候,先更新資料庫,然後再刪除快取。

為什麼是刪除快取,而不是更新快取?

這裡還會有另外一個問題:為什麼是刪除快取,而不是更新快取?

原因很簡單,很多時候,在複雜點的快取場景,快取不單單是資料庫中直接取出來的值。

比如可能更新了某個表的一個欄位,然後其對應的快取,是需要查詢另外兩個表的資料並進行運算,才能計算出快取最新的值的。

另外更新快取的代價有時候是很高的。是不是說,每次修改資料庫的時候,都一定要將其對應的快取更新一份?也許有的場景是這樣,但是對於比較複雜的快取資料計算的場景,就不是這樣了。如果你頻繁修改一個快取涉及的多個表,快取也頻繁更新。但是問題在於,這個快取到底會不會被頻繁訪問到?

舉個例子,一個快取涉及的表的欄位,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼快取更新 20 次、100 次;但是這個快取在 1 分鐘內只被讀取了 1 次,有大量的冷資料。實際上,如果你只是刪除快取的話,那麼在 1 分鐘內,這個快取不過就重新計算一次而已,開銷大幅度降低。用到快取才去算快取。

其實刪除快取,而不是更新快取,就是一個 lazy 計算的思想,不要每次都重新做複雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。像 mybatis,hibernate,都有懶載入思想。查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都把裡面的 1000 個員工的資料也同時查出來啊。80% 的情況,查這個部門,就只是要訪問這個部門的資訊就可以了。先查部門,同時要訪問裡面的員工,那麼這個時候只有在你要訪問裡面的員工的時候,才會去資料庫裡面查詢 1000 個員工。

嚴格要求 「快取 + 資料庫」 必須保持一致

這種模式應該就是我們大多數人現在使用的模式,在不是嚴格要求 「快取 + 資料庫」 必須保持一致的時候,這樣做是可以的。

那為什麼說上面這種方案在嚴格要求 「快取 + 資料庫」 的場景下不行呢?

因為這個問題只有在對一個資料在併發的進行讀寫的時候,才可能會出現這種問題。

就是如果說併發量沒那麼高的話,比如 1s 才一個對快取的讀寫請求,那麼大概率是不會出現快取 「快取 + 資料庫」 不一致的情況。

但是問題是,如果每天的是上億的流量,每秒併發讀是幾萬,每秒只要有資料更新的請求,就可能會出現上述的 「快取 + 資料庫」 不一致的情況。

那麼這種問題是不是就沒有解決方案,當然有,但是如果不是嚴格要求 「快取 + 資料庫」 必須保持一致性的場景,最好不要使用這個方案。

我們可以讓讀請求和寫請求序列化,把所有的讀寫請求都序列到一個佇列裡面去。

序列化可以保證一定不會出現不一致的情況,但是它也會導致系統的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。

把所有的操作都放到佇列裡面,順序肯定不會亂,但是併發高了,這佇列很容易阻塞,反而會成為整個系統的瓶頸。

參考

https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md

相關文章