1. 程式人生 > 其它 >快取與資料庫的一致性

快取與資料庫的一致性

對於讀多寫少的場景,快取是提升系統讀效能的一個常見技術。而資料系統一旦引入了新的元件,必然會帶來資料一致性的問題。這裡不考慮強一致性,強一致性帶來的效能問題在高併發的場景下是不可接受的,因此這裡說的是最終一致性。

對於快取和資料庫一起使用的模式,一般來說有以下幾種。其中最常用的應該是 Cache Aside Pattern。

Cache Aside Pattern

這種方案下,需要分別操作快取和資料庫。

  • 對於讀操作,一般都是先讀快取,如果命中則返回結果;如果 miss 則去讀庫,並將結果放入快取,然後再返回結果。這個沒什麼爭議。
  • 對於寫操作,則面臨兩個問題:
    • 一個是如何更新快取,是淘汰快取(讓下一個讀操作去更新)還是修改快取;
    • 一個該先更新資料庫還是該先更新快取

更新快取還是淘汰快取?

許多文章在分析淘汰還是更新時,舉了很多資料不一致的例子。其實更新快取和淘汰快取在資料一致性上沒有太大區別。對資料一致性有影響的是操作快取和操作資料庫的先後順序以及併發操作帶來的不確定性。更新快取和淘汰快取的區別在於成本:

  • 更新快取的優點會減少miss,但是有兩處成本的增加:
    • 有些快取的值計算較為複雜,可能消耗較大,沒必要放在更新操作中;
    • 有些資料可能是寫多讀少,那麼去頻繁更新不會被訪問的快取是效能上的浪費,而刪除快取在 Redis 上只是做一個標記,成本很低。
  • 淘汰快取的優點是簡單,而且減少不必要的快取寫入,缺點則是會造成一次 miss。雖然只有一個 miss,如果是熱點 key 的話還會引起快取擊穿的問題,可以用分散式鎖的方式或主備快取來解決,不過這是另一個議題。

因此一般選擇淘汰快取。

先更新資料庫還是先淘汰快取?

首先再明確一次,快取和資料庫不能放在一個事務中(即強一致性),這在併發情況下這是不可接受的。

Cache Aside Pattern 的要求是先更新資料庫後淘汰快取,這也是一般大家推薦的。因為即使在單機資料庫加 Redis 的情況下,先淘汰快取都會有不小的概率導致髒資料,比如更新操作 Q1 刪除了快取後,另一個讀操作 Q2 先遇到 miss,然後讀取了舊資料,此時 Q1 更新了資料庫,然後 Q2 更新了快取這樣的場景。因為這樣後面的操作都會讀髒資料。

當然先更新資料庫後淘汰快取也會出現極端情況,比如快取更新的套路中提到的“這個case理論上會出現,不過,實際上出現的概率可能非常低,因為這個條件需要發生在讀快取時快取失效,而且併發著有一個寫操作。而實際上資料庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入資料庫操作,而又要晚於寫操作更新快取,所有的這些條件都具備的概率基本並不大。”的場景。

上述情況描述的是單資料庫的情況下,資料庫和快取操作都能成功的前提下的取捨。實際場景中還有以下因素需要考慮:

  • 如果更新完了資料庫,刪除快取失敗了怎麼辦。
  • 如果資料庫是主從結構,就需要考慮主從延遲。

其實考慮了上面這些因素,先淘汰快取和後淘汰快取就都不絕對了,因為都需要一些其他機制來進行補充。最簡單的機制:快取設定較短的過期時間。這個適用於併發度不高的場景。最簡單並且肯定會達到最終一致性。另一種機制是延遲雙刪:

  1. 刪除快取
  2. 更新資料庫
  3. 睡眠一個特定時間
  4. 再刪除一次快取

這裡的特定時間,是需要根據具體情況衡量的,它需要大於主從延遲,加一次讀庫並寫入快取的時間,這樣才能避免沒有另一個程序,讀到了寫入之間的髒資料並更新到了快取中。

上面延遲雙刪會讓寫操作的RT值變高,因此可以將第二次刪除改成非同步的方式。可以通過傳送訊息佇列,或者訂閱 binlog 獲取通知的形式,來進行第二次刪除。此時會發現就跟之前說的一樣,先淘汰還是後淘汰並不是那麼絕對了。總結而言,最終可以採取的機制如下:

  1. 刪除快取(可選)
  2. 更新資料庫
  3. 傳送一條刪除快取訊息給刪除快取的服務/或者刪除快取的服務通過訂閱binlog得知資料庫的變更
  4. 刪除快取的服務拿到資料後,等待一個特定時間後進行刪除快取

Read/Write Through

PatternRead/Write Through 套路是把更新資料庫(Repository)的操作由快取自己代理了,所以,對於應用層來說,就簡單很多了。可以理解為,應用認為後端就是一個單一的儲存,而儲存自己維護自己的Cache。

  • Read Through:Read Through 套路就是在查詢操作中更新快取,也就是說,當快取失效的時候(過期或LRU換出),Cache Aside 是由呼叫方負責把資料載入入快取,而 Read Through 則用快取服務自己來載入,從而對應用方是透明的。
  • Write Through:Write Through 套路和 Read Through 相仿,不過是在更新資料時發生。當有資料更新的時候,如果沒有命中快取,直接更新資料庫,然後返回。如果命中了快取,則更新快取,然後再由Cache自己更新資料庫(這是一個同步操作)

Write Behind Caching Pattern

Write Behind 又叫 Write Back。類似 Linux 檔案系統的 Page Cache 的演算法。在更新資料的時候只更新快取,不更新資料庫,快取會非同步地批量更新資料庫。這種方式的優點在於效能極高,缺點在於資料會丟失,如果資料還沒來得及更新到資料庫服務就掛了,這些操作就丟失了。

非同步快取更新方案

所有的更新操作都走資料庫,然後有一個程式非同步的從資料庫更新到快取。這個程式可以通過訂閱 binlog 的形式來更新快取。這種方案也有很大的侷限性,因為這種方案下只適合資料量較小的,並且大部分資料都能被命中的情況。因為這種方案需要將所有資料都放入快取中,它沒有快取 miss 時的載入步驟。

參考

https://coolshell.cn/articles/17416.html
https://mp.weixin.qq.com/s/CY4jntpM7VNkBrz1FKRsOw
https://mp.weixin.qq.com/s/aJ33A5O2PUcUOA34kL-Nzw
https://mp.weixin.qq.com/s/4W7vmICGx6a_WX701zxgPQ