資料庫和快取一致性
快取
快取構建的基本思想是利用時間侷限性原理,通過空間換時間來達到加速資料獲取的目的,同時由於快取空間的成本較高,在實際設計架構中還要考慮訪問延遲和成本的權衡問題。
業務系統讀寫快取有 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 個執行緒要併發「讀寫」資料,可能會發生以下場景:
- 執行緒 A 要更新 X = 2(原值 X = 1)
- 執行緒 A 先刪除快取
- 執行緒 B 讀快取,發現不存在,從資料庫中讀取到舊值(X = 1)
- 執行緒 A 將新值寫入資料庫(X = 2)
- 執行緒 B 將舊值寫入快取(X = 1)
最終 X 的值在快取中是 1(舊值),在資料庫中是 2(新值),發生不一致。
可見,先刪除快取,後更新資料庫,當發生「讀+寫」併發時,還是存在資料不一致的情況。
解決方案:延時雙刪(沒法完全保證)
2、先更新資料庫,再刪快取
依舊是 2 個執行緒併發「讀寫」資料:
- 快取中 X 不存在(資料庫 X = 1)
- 執行緒A讀取資料庫,得到舊值(X=1)
- 執行緒 B 更新資料庫(X = 2)
- 執行緒 B 刪除快取
- 執行緒 A 將舊值寫入快取(X = 1)
最終 X 的值在快取中是 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