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

資料庫和快取一致性

快取

快取構建的基本思想是利用時間侷限性原理,通過空間換時間來達到加速資料獲取的目的,同時由於快取空間的成本較高,在實際設計架構中還要考慮訪問延遲和成本的權衡問題。

業務系統讀寫快取有 3 種模式:

  • Cache Aside(旁路快取),先更新db,後刪除快取
  • Read/Write Through(讀寫穿透),cache服務更新快取,並更新db。
  • Write Behind Caching(非同步快取寫入),cache服務甘心快取,非同步更新dn。

Cache Aside模式(旁路快取)

1.Write: 更新 DB 後,直接將 key 從 cache 中刪除,然後由 DB 驅動快取資料的更新;

2.Read: 是先讀 cache,如果 cache 沒有,則讀 DB,同時將從 DB 中讀取的資料回寫到 cache。

特點:

確保資料以DB 結果為準

適用場景:

對資料一致性要求比較高的業務,或者是快取資料更新比較複雜的業務,比如需要通過多個原始資料進行計算後設置的快取資料

Read/Write Through模式(讀寫穿透)

1. Write: 儲存服務收到業務應用的寫請求時,會首先查 cache,如果資料在 cache 中不存在,則只更新 DB,如果資料在 cache 中存在,則先更新 cache,然後更新 DB。

2. Read: 儲存服務收到讀請求時,如果命中 cache 直接返回,否則先從 DB 載入,回寫到 cache 後返回響應。

特點:

  • 儲存服務封裝了所有的資料處理細節,業務應用端程式碼只用關注業務邏輯本身,系統的隔離性更佳。
  • 進行寫操作時,如果 cache 中沒有資料則不更新,有快取資料才更新,記憶體效率更高。

適用場景:

使用者最新Feed列表

Write Behind Caching模式(非同步快取寫入)

1.Write: 只更新快取,不直接更新 DB,而是改為非同步批量的方式來更新 DB

2.Read:如果命中 cache 直接返回,否則先從 DB 載入,回寫到 cache 後返回響應。

特點:

寫效能最高,定期非同步重新整理,存在資料丟失概率

適用場景:

適合變更頻率特別高,但對一致性要求不太高的業務,特別是可以合併寫請求的業務,比如對一些計數業務

這裡用的最多的是旁路模式

快取與資料庫的一致性問題

1、先刪快取,再更新資料庫

如果有 2 個執行緒要併發「讀寫」資料,可能會發生以下場景:

  1. 執行緒 A 要更新 X = 2(原值 X = 1)
  2. 執行緒 A 先刪除快取
  3. 執行緒 B 讀快取,發現不存在,從資料庫中讀取到舊值(X = 1)
  4. 執行緒 A 將新值寫入資料庫(X = 2)
  5. 執行緒 B 將舊值寫入快取(X = 1)

最終 X 的值在快取中是 1(舊值),在資料庫中是 2(新值),發生不一致。

可見,先刪除快取,後更新資料庫,當發生「讀+寫」併發時,還是存在資料不一致的情況。

解決方案:延時雙刪(沒法完全保證)

2、先更新資料庫,再刪快取

依舊是 2 個執行緒併發「讀寫」資料:

  1. 快取中 X 不存在(資料庫 X = 1)
  2. 執行緒A讀取資料庫,得到舊值(X=1)
  3. 執行緒 B 更新資料庫(X = 2)
  4. 執行緒 B 刪除快取
  5. 執行緒 A 將舊值寫入快取(X = 1)

最終 X 的值在快取中是 1(舊值),在資料庫中是 2(新值),也發生不一致。

這種情況「理論」來說是可能發生的,但實際真的有可能發生嗎?

其實概率「很低」,這是因為它必須滿足 3 個條件:

  1. 快取剛好已失效
  2. 讀請求 + 寫請求併發
  3. 更新資料庫 + 刪除快取的時間(步驟 3-4),要比讀資料庫 + 寫快取時間長(步驟 2 和 5)

也就是步驟5通常會在步驟四的前面。

這種方案併發條件下資料一致性的可能性很小,但第二步執行失敗會導致資料一致性的問題。

解決方案

(1)、訊息佇列

非同步重試,確保步驟二成功

訊息可靠性投遞,確保消費者成功消費訊息。

(2)binlog日誌 + MQ

訂閱資料庫變更日誌,再操作快取。具體來講就是,我們的業務應用在修改資料時,「只需」修改資料庫,無需操作快取。

可以做到強一致嗎?

  效能和一致性不能同時滿足,為了效能考慮,通常會採用「最終一致性」的方案。

  要想做到強一致,最常見的方案是 2PC、3PC、Paxos、Raft 這類一致性協議,但它們的效能往往比較差,而且這些方案也比較複雜,還要考慮各種容錯問題。

相反,這時我們換個角度思考一下,我們引入快取的目的是什麼?

沒錯,效能

  一旦我們決定使用快取,那必然要面臨一致性問題。效能和一致性就像天平的兩端,無法做到都滿足要求。

而且,就拿我們前面講到的方案來說,當操作資料庫和快取完成之前,只要有其它請求可以進來,都有可能查到「中間狀態」的資料。

  所以如果非要追求強一致,那必須要求所有更新操作完成之前期間,不能有「任何請求」進來。

  雖然我們可以通過加「分佈鎖」的方式來實現,但我們要付出的代價,很可能會超過引入快取帶來的效能提升。

  所以,既然決定使用快取,就必須容忍「一致性」問題,我們只能儘可能地去降低問題出現的概率。

  同時我們也要知道,快取都是有「失效時間」的,就算在這期間存在短期不一致,我們依舊有失效時間來兜底,這樣也能達到最終一致。

參考:

https://mp.weixin.qq.com/s/D4Ik6lTA_ySBOyD3waNj1w

https://mp.weixin.qq.com/s/dYvM8_6SQnYRB6KjPsprbw