緩存使用問題 —— 緩存一致性問題解決方案

1、理解緩存使用場景及一致性問題

數據庫存儲通常支持完整的ACID特性,因爲可靠性、持久性等因素,性能普遍不高,高併發的查詢會給數據庫帶來壓力,造成數據庫系統的不穩定。同時也容易產生延遲。 根據局部性原理,80%請求會落到20%的熱點數據上,在讀多寫少場景,在請求數據庫之前增加一層緩存非常有助提升系統吞吐量和健壯性。

存儲的數據隨着時間可能會發生變化,而緩存中的數據就會不一致。具體能容忍的不一致時間,需要具體業務具體分析,但是通常的業務,都需要做到最終一致性。

2、尋找解決方案(選擇Mysql數據庫,Redis作爲數據緩存)

方案一:設置Redis中key的過期時間,寫->Mysql更新,讀->Redis沒有key時,從Mysql取值新增key;

不足:完全依賴過期時間,時間太短容易緩存頻繁失效;時間太長更新延遲導致數據不一致;

方案二:以下方案均在方案一的基礎上擴展,key過期時間作爲兜底。寫->Mysql更新,且更新Redis;

優點:較方案一,更新延遲更小,不一致問題減少;

不足:

  • 當Reids更新失敗,就退化到了方案一的問題;
  • 同時有寫操作請求A和寫操作請求B進行Mysql更新操作,A更新Mysql——B更新Mysql——B更新緩存——A更新緩存,出現請求A更新緩存在請求B之後,導致髒數據;(集中寫操作,導致緩存頻繁更新,數據讀取不到。更新Redis之前計算邏輯過於複雜耗時,影響緩存使用性能)
  1. 引入消息隊列(保證可靠性),將Redis更新操作交給諸如kafaka的消息隊列,再搭建一個服務消費,去異步更新Redis。
  2. 在多臺服務器併發情況下還是不能保證時序性,此時可以通過訂閱binlog來更新Redis,搭建的消費服務作爲Mysql的一個Slave,訂閱binlog解析出更新內容,再更新到Redis

方案三:寫->先刪除Redis,再更新Mysql,讀->Redis沒有key時,從Mysql取值新增key;

優點:較方案一,更新延遲更小,不一致問題減少;

不足:同時有請求寫操作A和讀操作B,A刪除Redis中key——B發現key不存在——B獲取Msql舊值——B將舊值key寫入Redis——A更新Mysql,導致數據不一致問題,好在有過期時間,否則Redis永遠都是髒數據。此處可以通過雙刪策略來處理,看一下僞代碼:

public void write(String key,Object data){

  redis.delKey(key);

  db.updateData(data);

  Thread.sleep(1000);

  redis.delKey(key);

}

將1秒內所造成的緩存髒數據刪除步驟中,1秒是怎麼確定的?就需要評估項目的讀操作邏輯耗時,然後就在此處設置爲讀耗時加幾百毫秒。這麼做的目的是確保讀請求結束,寫請求可以刪除讀請求造成的緩存髒數據。

延伸問題:如果Mysql的讀操作是數據分離架構怎麼辦?同時有請求寫操作A和讀操作B,A刪除Redis中key——A更新Mysql——B發現Redis中key不存在——B獲取未同步從庫的Msql舊值——B將舊值key寫入Redis,還是使用雙刪延時策略,只是將睡眠時間改爲主從同步時間加幾百毫秒。採用同步淘汰策略,發現吞吐量降低怎麼辦?可以將第二次刪除作爲異步去處理,寫操作就不需要睡眠了。但要是第二次刪除失敗怎麼辦?失敗又會導致數據不一致,那麼又要交給消息隊列去重試

方案四:寫->更新Mysql,再刪除Redis,讀->Redis沒有key時,從Mysql取值新增key;

優點:較方案一,更新延遲更小,不一致問題減少;

不足:

  • 同時有讀操作A和寫操作B兩個請求,緩存剛好失效——A讀取Msql舊值——B將新值寫入Mysql——B刪除Redis中key——A將舊值寫入Redis中,又又又發生了髒數據,但發生上述情況就要有天生條件:讀數據庫操作耗時大於寫數據庫耗時。但常規情況,數據庫讀操作是遠遠快於寫操作的,因此這種情況很難出現。即使出現,也可以使用上述的異步延時刪除策略,保證讀請求完成後再進行刪除操作。
  • 刪除緩存失敗了,B將新值寫入Mysql——B刪除Redis中key失效——A直接從Reids中取值了,有兩種解決方案:
  1. 讓消息隊列不斷去重試
  2. 通過訂閱binlog解析出數據庫執行更新內容,再放到消息隊列,最後更新到Redis

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章