原因:
用緩存,主要有兩個用途:高性能、高並發。
高性能
非實時變化的數據-查詢mysql耗時需要300ms,存到緩存redis,每次查詢僅僅1ms,性能瞬間提升百倍。
高並發
mysql 單機支撐到2K QPS就容易報警了,如果系統中高峰時期1s請求1萬,僅單機mysql是支撐不了的,但是使用緩存的話,單機支撐的並發量輕松1s幾萬~十幾萬。
原因是緩存位於內存,內存對高並發的良好支持。
常見的緩存問題:
1、緩存與數據庫雙寫不一致
2、緩存雪崩、緩存穿透
3、緩存並發競爭
1、如何保證緩存與數據庫的雙寫一致性?
最經典的緩存+數據庫讀寫的模式,就是 Cache Aside Pattern。
- 讀的時候,先讀緩存,緩存沒有的話,就讀數據庫,然后取出數據后放入緩存,同時返回響應。
- 更新的時候,先更新數據庫,然后再刪除緩存。
為什么是刪除緩存,而不是更新緩存?-> 用到緩存才去算緩存-lazy加載思想。
非高並發場景數據不一致問題:
先修改數據庫,再刪除緩存。如果緩存刪除失敗,導致緩存中是舊數據。
解決方法:
先刪除緩存,再修改數據庫。如果緩存刪除失敗,則整個操作失敗,如果修改數據庫失敗,緩存已為空,則請求數據時,會重新加載數據庫的數據,
雖然都是舊數據,但保持了數據一致性。
高並發場景數據不一致問題:
先刪除了緩存,然后要去修改數據庫,此時還沒修改。(定義為步驟A)
一個請求過來,去讀緩存,發現緩存空了,去查詢數據庫(定義為步驟B1)。查到了修改前的舊數據,放到了緩存中。(定義為步驟B2)
隨后數據變更的程序完成了數據庫的修改。此時數據庫和緩存數據不一致了。
解決方法:
定義一個FIFO的阻塞隊列,例如:LinkedBlockingQueue,將步驟A和步驟B放入同一個隊列中。步驟A必然在步驟B的前面。
當場景發生了上述步驟B1時,只有2個情況:緩存已刪除,數據庫已修改或者數據庫還未修改。不考慮已修改的正常情況,則步驟A必然已發生。
則可以在,步驟A和步驟B1發生時均按照數據唯一標識(ID)入同一個隊列。步驟A先於步驟B1入隊,按照FIFO的方式,步驟A會先完成,則問題
理論上得以解決。
注意事項:
- 高並發場景下肯定會有同一個數據多個步驟B的出現,可以過濾去重。即:隊列中已存在則不用再入隊了。
- 由於步驟B已變成異步讀請求,基於我們的高並發場景,需要考慮讀超時的問題。如果請求還在等待時間范圍內,不斷輪詢發現可以取到值了,那么就直接返回;如果請求等待的時間超過一定時長,那么這一次直接從數據庫中讀取當前的舊值。
- 如果大量的數據更新頻繁,導致隊列中堆積大量的更新操作,然后大量的讀請求超時,最后導致大量的請求直接走數據庫。則需要根據具體業務模擬測試峰值,部署多個應用分攤更新操作。
2、緩存雪崩、緩存穿透-> 請移步這篇文章 緩存雪崩、緩存穿透
3、Redis的並發競爭問題
場景:
- redis的並發競爭問題,主要是發生在並發寫競爭。
- redis本身是單線程,不存在並發問題,但我們在使用過程中會存在並發問題:更新操作分成了3步驟,讀取數據,數據操作,設新值回去。
例如:redis有一個key=“product_num”,value=10, 此時有2個客戶端同時對這個key做加1操作,預期結果是value=12。
但有這樣的情況:第一個客戶端還未設新值回去的時候第2個客戶端獲取到值,為10,則2個客戶端最終操作結果value=11,與預期不符!
解決方案:
- 利用redis自帶的 incr 命令。
- CAS樂觀鎖。
某個時刻,多個系統實例都去更新某個 key。可以基於 zookeeper 實現分布式鎖。每個系統通過 zookeeper 獲取分布式鎖,確保同一時間,只能有一個系統實例在操作某個 key,別人都不允許讀和寫。
另:寫mysql數據庫時保存一個時間戳(或者version),從 mysql 查詢的時候,時間戳也帶出來。
每次要寫DB前,先判斷一下當前這個 value 的時間戳是否比緩存里的 value 的時間戳要新。如果是的話,那么可以寫,否則,就不能用舊的數據覆蓋新的數據。