緩存擊穿問題概述
緩存擊穿是指某個 熱點數據緩存過期 時,大量并發請求直接穿透緩存,同時訪問數據庫,導致數據庫壓力驟增甚至崩潰。以下是基于 互斥鎖 和 邏輯過期 的解決方案:
一、緩存擊穿的核心原因
- 熱點數據失效:高并發場景下,某個熱點數據的緩存突然過期,導致瞬時大量請求繞過緩存直接訪問數據庫。
- 無緩存保護:緩存未設計有效機制應對突發性并發穿透。
二、解決方案 1:互斥鎖(Mutex Lock)
核心思想
當緩存失效時,僅允許一個線程查詢數據庫并重建緩存,其他線程阻塞等待,避免并發穿透。
實現步驟(以 Redis 分布式鎖為例)
-
查詢緩存
檢查緩存是否存在,若存在則直接返回數據。def get_data(key):data = redis.get(key)if data is not None:return data# 緩存未命中,嘗試加鎖return get_data_via_mutex(key)
-
獲取分布式鎖
使用 Redis 的SETNX
(或 Redlock 算法)實現分布式鎖,確保原子性。def acquire_lock(lock_key, expire=10):# 生成唯一標識(如UUID),避免誤刪其他線程的鎖identifier = str(uuid.uuid4())if redis.set(lock_key, identifier, nx=True, ex=expire):return identifierreturn None
-
查詢數據庫并重建緩存
只有獲取鎖的線程執行數據庫查詢,其他線程等待。def get_data_via_mutex(key):lock_key = f"lock:{key}"identifier = acquire_lock(lock_key)if not identifier:# 未獲取鎖,短暫等待后重試time.sleep(0.1)return get_data(key)try:# 再次檢查緩存(可能已被其他線程更新)data = redis.get(key)if data:return data# 查詢數據庫data = db.query("SELECT * FROM table WHERE key=?", key)# 寫入緩存redis.setex(key, 3600, data)finally:# 釋放鎖(需原子性操作)release_lock(lock_key, identifier)return data
-
釋放鎖
使用 Lua 腳本確保原子性釋放鎖:-- KEYS[1]=lock_key, ARGV[1]=identifier if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1]) elsereturn 0 end
優缺點
- 優點:強一致性,徹底避免緩存擊穿。
- 缺點:性能有損耗(線程需等待鎖),鎖超時時間需合理設置(過長影響并發,過短可能死鎖)。
三、解決方案 2:邏輯過期(Logical Expiration)
核心思想
緩存數據不依賴物理過期時間,而是在數據中存儲邏輯過期時間。當數據過期時,由單個線程異步更新緩存,其他線程繼續返回舊數據。
實現步驟
-
緩存數據結構設計
在緩存中存儲邏輯過期時間(expire_time
)和實際數據:{"data": "真實數據","expire_time": 1715000000 // 邏輯過期時間戳 }
-
查詢緩存
檢查邏輯過期時間,若未過期則直接返回數據。def get_data(key):cache_data = redis.get(key)if cache_data:data_obj = json.loads(cache_data)if data_obj["expire_time"] > time.time():return data_obj["data"]else:# 觸發異步更新async_refresh_cache(key)return data_obj["data"] # 返回舊數據else:# 緩存完全不存在(需初始化)return load_data_and_init_cache(key)
-
異步更新緩存
使用互斥鎖或標記位,確保僅一個線程執行數據庫查詢。def async_refresh_cache(key):lock_key = f"refresh_lock:{key}"if redis.set(lock_key, 1, nx=True, ex=10):try:# 查詢數據庫new_data = db.query("SELECT * FROM table WHERE key=?", key)# 更新邏輯過期時間(例如延長1小時)new_expire_time = time.time() + 3600cache_obj = {"data": new_data, "expire_time": new_expire_time}redis.setex(key, 3600 * 24, json.dumps(cache_obj)) # 物理過期時間更長finally:redis.delete(lock_key)
-
初始化緩存
若緩存完全不存在(如服務重啟),直接加載數據:def load_data_and_init_cache(key):# 加鎖防止并發初始化lock_key = f"init_lock:{key}"identifier = acquire_lock(lock_key)if not identifier:time.sleep(0.1)return get_data(key)try:# 再次檢查緩存cache_data = redis.get(key)if cache_data:return json.loads(cache_data)["data"]# 查詢數據庫并寫入緩存data = db.query("SELECT * FROM table WHERE key=?", key)expire_time = time.time() + 3600cache_obj = {"data": data, "expire_time": expire_time}redis.setex(key, 3600 * 24, json.dumps(cache_obj))return datafinally:release_lock(lock_key, identifier)
優缺點
- 優點:高并發性能好,用戶無感知短暫延遲。
- 缺點:數據可能短暫不一致,需業務容忍舊數據。
四、方案對比與選型
方案 | 適用場景 | 一致性 | 性能 | 實現復雜度 |
---|---|---|---|---|
互斥鎖 | 強一致性要求(如金融交易) | 強一致性 | 較低 | 中 |
邏輯過期 | 高并發、允許短暫不一致 | 最終一致性 | 高 | 高 |
五、增強策略
-
結合兩種方案
- 在邏輯過期的基礎上,若異步更新失敗,可降級為互斥鎖同步更新。
-
熔斷機制
- 當數據庫壓力過大時,觸發熔斷,直接返回默認值或錯誤頁。
-
預熱緩存
- 提前加載熱點數據,避免緩存突然過期。
六、注意事項
-
鎖超時時間
- 互斥鎖的超時時間需略大于數據庫查詢時間,避免鎖提前釋放導致多個線程同時更新。
-
緩存雪崩防護
- 為不同 Key 設置隨機過期時間,避免大量緩存同時失效。
-
邏輯過期時間維護
- 異步更新失敗時,需記錄日志并告警,防止緩存長期不更新。
通過合理選擇互斥鎖或邏輯過期方案,可有效解決緩存擊穿問題,保障系統的高可用性與穩定性。