一、引言
今天我們來聊聊一個在分布式系統中非常常見但又十分棘手的問題——Redis與MySQL之間的雙寫一致性。我們在項目中多多少少都遇到過類似的困擾,緩存是用Redis,數據庫是用MySQL,但如何確保兩者之間的數據一致性呢?接下來我會盡量簡潔地為大家解析這個問題,并提供幾個實戰方案。
二、雙寫一致性挑戰
我們先來看看什么是雙寫一致性。
簡單來說,就是當數據同時存在于緩存(Redis)和數據庫(MySQL)時,如何確保這兩者之間的數據是一致的。
典型場景
- 寫數據庫后忘記更新緩存:這種情況最常見,當我們更新數據庫后,緩存沒有同步更新,導致讀取到舊的數據。
- 刪除緩存后數據庫更新失敗:在某些操作中,我們可能會先刪除緩存,再更新數據庫,但如果數據庫更新失敗,就會導致緩存和數據庫的數據不一致。
在了解完雙寫一致性帶來的挑戰之后我們接下來看看幾種經典的緩存模式。
三、緩存模式
3.1?Cache Aside Pattern (旁路模式)
Cache Aside Pattern是最常見的一種緩存使用模式,它的核心思想是以數據庫為主,緩存為輔。
工作流程
讀取操作:先從緩存中讀取數據,如果緩存命中則返回結果;如果緩存未命中,則從數據庫中讀取數據,并將數據寫入緩存。
更新操作:先更新數據庫,再刪除緩存中的舊數據。
示例代碼:
public class CacheAsidePattern {private RedisService redis;private DatabaseService database;// 讀取操作public String getData(String key) {// 從緩存中獲取數據String value = redis.get(key);if (value == null) {// 緩存未命中,從數據庫獲取數據value = database.get(key);if (value != null) {// 將數據寫入緩存redis.set(key, value);}}return value;}// 更新操作public void updateData(String key, String value) {// 更新數據庫database.update(key, value);// 刪除緩存中的舊數據redis.delete(key);}
}
優缺點分析:
優點:
- 簡單易懂,易于實現。
- 讀性能高,因為大部分讀操作都會命中緩存。
缺點:
- 存在短暫的不一致情況,更新數據庫后緩存可能還沒刪除。
- 刪除緩存后,如果數據庫更新失敗,會導致數據不一致。
3.2?讀寫穿透模式
3.2.1 寫穿透
當緩存未命中時,自動從數據庫加載數據,并寫入緩存。
3.2.2 讀穿透
當緩存更新時,同步將數據寫入數據庫。
3.2.3 代碼示例
public class ReadWriteThroughPattern {private RedisService redis;private DatabaseService database;// Read-Throughpublic String readThrough(String key) {// 從緩存中獲取數據String value = redis.get(key);if (value == null) {// 緩存未命中,從數據庫獲取數據value = database.get(key);if (value != null) {// 將數據寫入緩存redis.set(key, value);}}return value;}// Write-Throughpublic void writeThrough(String key, String value) {// 將數據寫入緩存redis.set(key, value);// 同步將數據寫入數據庫database.update(key, value);}
}
3.2.4?優缺點分析
優點:
- 保證了數據的強一致性,緩存和數據庫的數據始終同步。
- 讀寫操作都由緩存處理,數據庫壓力較小。
缺點:
- 寫操作的延遲較高,因為每次寫入緩存時都需要同步寫入數據庫。
- 實現復雜度較高,需要額外的緩存同步機制。
3.3?異步緩存寫入(Write Behind)
緩存更新后,異步批量寫入數據庫。這種策略適用于可以容忍一定數據不一致的高性能場景。
3.3.1 示例代碼
public class WriteBehindPattern {private RedisService redis;private DatabaseService database;private UpdateQueue updateQueue;// 異步緩存寫入public void writeBehind(String key, String value) {// 將數據寫入緩存redis.set(key, value);// 異步將數據寫入數據庫asyncDatabaseUpdate(key, value);}private void asyncDatabaseUpdate(String key, String value) {// 異步操作,將更新請求放入隊列updateQueue.add(new UpdateTask(key, value));}
}
3.3.2?優缺點分析
優點:
- 寫操作的性能非常高,因為只需更新緩存,數據庫更新是異步進行的。
- 適用于對寫操作性能要求較高的場景。
缺點:
- 存在數據不一致的風險,緩存更新后數據庫可能還未更新。
- 實現復雜度較高,需要處理異步操作中的異常和重試。
四、實戰解析
4.1?延時雙刪策略
延時雙刪策略的核心思想是:在更新數據庫后,先刪除一次緩存,然后延遲一段時間再刪除一次緩存,減少數據不一致的風險。
關鍵是如何確定延遲時間,這個時間需要根據系統的具體情況來調整,以平衡一致性和性能。
public class DelayedDoubleDeletePattern {private RedisService redis;private DatabaseService database;private ScheduledExecutorService scheduledExecutorService;private long delay = 500; // 延遲時間,單位:毫秒// 更新操作public void updateDataWithDelay(String key, String value) {// 更新數據庫database.update(key, value);// 刪除緩存中的舊數據redis.delete(key);// 延遲一段時間再刪除緩存scheduledExecutorService.schedule(() -> redis.delete(key), delay, TimeUnit.MILLISECONDS);}
}
優缺點分析:
優點:
- 簡化了緩存和數據庫的一致性問題。
- 避免了緩存和數據庫的同步更新,提高了系統性能。
缺點:
- 需要精確控制延遲時間,否則可能導致緩存和數據庫不一致。
- 實現相對復雜,需要額外的定時任務管理。
4.2?刪除緩存重試機制
刪除緩存時,如果失敗,可以設置重試機制,以確保緩存最終被刪除。
通過使用Spring的@Retryable注解,可以簡化重試邏輯。
代碼示例:
public class CacheService {private RedisService redis;@Retryable(value = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 2000))public void deleteCache(String key) {// 刪除緩存中的數據redis.delete(key);}
}
優缺點分析:
優點:
- 確保緩存最終被刪除,降低數據不一致的風險。
- 使用Spring的重試機制,簡化實現邏輯。
缺點:
- 需要處理重試的多次失敗情況,可能導致系統負載增加。
- 適用于緩存刪除失敗率較低的場景。
4.3?監聽binlog異步刪除緩存
利用數據庫的binlog變更來異步更新緩存,通過消息隊列和異步服務解耦緩存更新操作。
通過訂閱binlog,將變更記錄放入消息隊列,然后由異步服務處理緩存更新。
代碼示例:
public class BinlogListenerPattern {private RedisService redis;private MessageQueue messageQueue;// 訂閱binlogpublic void onBinlogChange(BinlogEntry entry) {// 將變更記錄放入消息隊列messageQueue.send(new CacheUpdateMessage(entry.getKey()));}// 異步服務處理緩存更新public void processCacheUpdate(CacheUpdateMessage message) {// 刪除緩存中的數據redis.delete(message.getKey());}
}
優缺點分析:
優點:
- 利用數據庫的變更日志,保證緩存和數據庫的一致性。
- 異步處理提高了系統性能,降低了實時更新的壓力。
缺點:
- 實現復雜度較高,需要處理消息隊列和異步服務。
- 存在延遲更新的情況,可能導致短時間內的數據不一致。
五、小結
在實際項目中,我們需要根據具體的業務場景來選擇最合適的一致性策略。同時,在高并發場景下,可以結合分布式鎖和消息隊列來確保數據一致性。異步處理中的異常處理和重試策略也非常重要,能夠有效提高系統的穩定性和可靠性。
此外,在實際開發應用時,不需要自己再去實現一套緩存管理代碼,有很多框架已經提供了基于聲明式注解的緩存管理器抽象,只需要添加幾個注解,就可以實現數據庫緩存,例如:
- Spring Cache
- Alibaba Jetcache