前文回顧
建議前面文章沒看過的同學先看下前面的文章:
「老司機帶你玩轉面試(1):快取中介軟體 Redis 基礎知識以及資料持久化」
「老司機帶你玩轉面試(2):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