當執行寫操作後,需要保證從緩存讀取到的數據與數據庫中持久化的數據是一致的,因此需要對緩存進行更新。
因爲涉及到數據庫和緩存兩步操作,難以保證更新的原子性。
在設計更新策略時,我們需要考慮多個方面的問題:
- 對系統吞吐量的影響:比如更新緩存策略產生的數據庫負載小於刪除緩存策略的負載
- 併發安全性:併發讀寫時某些異常操作順序可能造成數據不一致,如緩存中長期保存過時數據
- 更新失敗的影響:若某個操作失敗,如何對業務影響降到最小
- 檢測和修復故障的難度: 操作失敗導致的錯誤會在日誌留下詳細的記錄容易檢測和修復。併發問題導致的數據錯誤沒有明顯的痕跡難以發現,且在流量高峯期更容易產生併發錯誤產生的業務風險較大。
更新緩存有兩種方式:
- 刪除失效緩存: 讀取時會因爲未命中緩存而從數據庫中讀取新的數據並更新到緩存中
- 更新緩存: 直接將新的數據寫入緩存覆蓋過期數據
更新緩存和更新數據庫有兩種順序:
- 先數據庫後緩存
- 先緩存後數據庫
兩兩組合共有四種更新策略,現在我們逐一進行分析。
併發問題通常由於後開始的線程卻先完成操作導致,我們把這種現象稱爲“搶跑”。 下面我們逐一分析四種策略中“搶跑”帶來的錯誤。
先更新數據庫,再刪除緩存
若數據庫更新成功,刪除緩存操作失敗,則此後讀到的都是緩存中過期的數據,造成不一致問題。
可能發生的併發錯誤:
時間 | 線程A | 線程B | 數據庫 | 緩存 |
---|---|---|---|---|
1 | 緩存失效 | v1 | null | |
2 | 從數據庫讀取v1 | v1 | null | |
3 | 更新數據庫 | v2 | null | |
4 | 刪除緩存 | v2 | null | |
5 | 寫入緩存 | v2 | v1 |
先更新數據庫,再更新緩存
同刪除緩存策略一樣,若數據庫更新成功緩存更新失敗則會造成數據不一致問題。
可能發生的併發錯誤:
時間 | 線程A | 線程B | 數據庫 | 緩存 |
---|---|---|---|---|
0 | v0 | v0 | ||
1 | 更新數據庫爲 v1 | v1 | v0 | |
2 | 更新數據庫爲 v2 | v2 | v0 | |
3 | 更新緩存爲 v2 | v2 | v2 | |
4 | 更新緩存爲 v1 | v2 | v1 |
當兩個寫線程發生衝突時,可以通過比較數據版本方式避免線程A寫入舊的數據。
先刪除緩存,再更新數據庫
可能發生的併發錯誤:
時間 | 線程A | 線程B | 數據庫 | 緩存 |
---|---|---|---|---|
1 | 刪除緩存 | v1 | null | |
2 | 緩存失效 | v1 | null | |
3 | 從數據庫讀取v1 | v1 | null | |
4 | 更新數據庫爲v2 | v2 | null | |
5 | 將v1寫入緩存 | v2 | v1 |
先更新緩存,再更新數據庫
若緩存更新成功數據庫更新失敗, 則此後讀到的都是未持久化的數據。因爲緩存中的數據是易失的,這種狀態非常危險。
因爲數據庫因爲鍵約束導致寫入失敗的可能性較高,所以這種策略風險較大。
可能發生的併發錯誤:
時間 | 線程A | 線程B | 數據庫 | 緩存 |
---|---|---|---|---|
0 | v0 | v0 | ||
1 | 更新緩存爲 v1 | v0 | v1 | |
2 | 更新緩存爲 v2 | v0 | v2 | |
3 | 更新數據庫爲 v2 | v2 | v2 | |
4 | 更新數據庫爲 v1 | v1 | v2 |
異步更新
雙寫更新的邏輯複雜,一致性問題較多。現在我們可以採用訂閱數據庫更新的方式來更新緩存。
阿里巴巴開源了mysql數據庫binlog的增量訂閱和消費組件 - canal。
我們可以採用API服務器只寫入數據庫,而另一個線程訂閱數據庫 binlog 增量進行緩存更新的策略。
這種策略存在和先更新數據庫後刪除緩存類似的併發問題:
時間 | 讀線程 | 寫線程 | 異步線程 | 數據庫 | 緩存 |
---|---|---|---|---|---|
1 | 緩存失效 | v1 | null | ||
2 | 從數據庫讀取v1 | v1 | null | ||
3 | 更新數據庫爲v2 | v2 | null | ||
4 | 刪除緩存/更新緩存 | v2 | null | ||
5 | 寫入緩存 | v2 | v1 |
這個問題同樣可以採用異步線程更新緩存,且寫入緩存時比較數據版本的方法來解決。