在現代互聯網應用中,Redis 緩存幾乎是性能優化的標配。但在使用過程中,一個繞不過去的問題就是:
如何保證 Redis 緩存與數據庫之間的數據一致性?
特別是在高并發場景下,讀寫操作錯位可能導致緩存中出現臟數據,影響業務正確性。
問題背景
我們通常使用 Cache Aside 模式(旁路緩存):
-
讀:先查緩存,沒有再查數據庫并寫入緩存
-
寫:更新數據庫,再刪除緩存
這個寫入邏輯看似合理,但卻存在很多坑。一不小心就會導致“緩存臟讀”或“數據回滾”的問題。
方案一:先更新數據庫,再刪除緩存
這是很多開發者最初采用的方式。
🧠 原理:
updateDB(key, newValue)
deleteCache(key)
邏輯非常簡單:先改數據庫,再刪緩存,期待下次讀取會重新從數據庫加載正確數據。
🖼? 圖解:
用戶請求更新數據|
[1] 更新數據庫 (新數據寫入)|
[2] 刪除 Redis 緩存
? 存在問題:
并發場景:
[1] updateDB(key, newValue) ? 更新成功
[2] deleteCache(key) ? 失敗,緩存未清除隨后:
[3] 用戶查詢 getCache(key) → 命中舊緩存 ?
結果是:
-
數據庫中是新值
-
緩存中是舊值,長時間存在
-
系統返回的是錯誤的數據
方案二:先刪除緩存,再更新數據庫
🧠 原理:
deleteCache(key)
updateDB(key, newValue)
🖼? 圖解:
用戶請求更新數據|
[1] 刪除 Redis 緩存|
[2] 更新數據庫
? 存在問題:
并發場景:
Time ---------->
A: deleteCache(new) -------->
B: getCache(old) ------------>
B: writeCache(old) ------------>
A: updateDb() ---------------->
-
線程 A 刪除緩存, 并在之后更新數據庫;
-
線程 B 在 A 刪除緩存之前讀取了舊緩存,隨后把舊值寫回緩存;
-
最終緩存中是 舊數據,數據庫是新數據 → ? 數據不一致!
方案三:延遲雙刪(延遲兜底)
延遲雙刪(Delayed Double Delete)是對Cache Aside模式的增強,通過兩次刪除緩存操作來減少不一致時間窗口。
實現步驟
1. 第一次刪除:在更新數據庫前,先刪除緩存
2. 更新數據庫:執行實際的數據庫更新操作
3. 延遲第二次刪除:在數據庫更新完成后,延遲一段時間再次刪除緩存
public void updateData(Data newData) {// 第一次刪除緩存cache.delete(newData.getId());// 更新數據庫database.update(newData);// 延遲第二次刪除executor.schedule(() -> {cache.delete(newData.getId());}, 500, TimeUnit.MILLISECONDS); // 延遲500ms
}
為什么需要延遲
延遲的目的是為了處理以下場景:
1. 在第一次刪除后、數據庫更新完成前,可能有請求讀取了舊數據并重新填充緩存
2. 數據庫主從復制延遲可能導致從庫讀取到舊數據
通過延遲第二次刪除,可以清除這些潛在的不一致情況。
延遲時間如何確定
延遲時間應考慮:
- 數據庫主從復制延遲時間(通常100-500ms)
- 業務對一致性的要求程度
- 系統負載情況
延遲雙刪的優化與變種
異步重試機制
當第二次刪除失敗時,可以采用異步重試機制確保最終一致性:
?
public void deleteWithRetry(String key, int maxRetries) {int retries = 0;while (retries < maxRetries) {try {cache.delete(key);break;} catch (Exception e) {retries++;if (retries >= maxRetries) {// 記錄失敗日志或放入死信隊列log.error("Failed to delete cache after {} retries", maxRetries);break;}Thread.sleep(100 * retries); // 指數退避}}
}
方案四:分布式鎖(強一致)
🧠 原理:
lock(key)
deleteCache(key)
updateDB(key)
unlock(key)
🖼? 圖解:
用戶請求更新數據|
[1] 獲取分布式鎖|
[2] 刪除緩存|
[3] 更新數據庫|
[4] 釋放鎖
? 優點:
-
寫操作串行化,強一致;
-
不會發生并發導致的數據回滾。
? 缺點:
-
實現復雜;
-
性能開銷大,需保證鎖系統高可用。
方案五:監聽 Binlog 回刷緩存(如使用 Canal)
🧠 原理:
MySQL 更新數據↓
產生 Binlog↓
Canal 監聽變更事件↓
主動刪除或刷新緩存
🖼? 圖解:
數據庫更新|
[1] 生成 Binlog|
[2] Canal 監聽 Binlog|
[3] 刪除/刷新 Redis 緩存
? 優點:
-
非侵入式,一致性強;
-
精準捕獲變化,自動驅動緩存刷新。
? 缺點:
-
運維成本高;
-
延遲取決于 Canal 拉取速度。
各方案對比總結
方案 | 是否一致 | 并發安全 | 實現復雜度 | 備注 |
---|---|---|---|---|
先更新 DB 再刪緩存 | ? 否 | 否 | 簡單 | 常見誤區,不能用 |
先刪緩存再更新 DB | ?? 部分 | 一定程度 | 簡單 | 可配合延遲雙刪使用 |
延遲雙刪 | ? 最終一致 | 較高 | 簡單 | 推薦大部分場景 |
分布式鎖 | ? 強一致 | 是 | 中等 | 對性能影響較大 |
Binlog + Canal 回刷緩存 | ? 強一致 | 是 | 高 | 推薦核心數據系統使用 |
總結
Redis 緩存作為提升系統性能的利器,也帶來了“一致性”的挑戰。掌握各種一致性方案,能讓你在面對不同業務需求時游刃有余。
🔑 核心思想是:避免緩存與數據庫數據錯位時被讀取或誤寫回緩存。