首先我們先來説一下什麼是緩存雙寫,就是我們使用redis的情況下一定會使用一個持久化的數據庫,最典型的就是redis+mysql的組合,使用他們倆就一定會存在數據不一致的情況,我們為了業務要求必須保證最終一致性,所以需要我們解決的就是使用什麼方法讓他們之間的數據儘可能的在最短的時間、最大的吞吐量、最安全的方式下保證數據的一致性。
關於策略就有同步和異步的方式,同步的方式:新增/查詢的情況下,如果redis沒有就操作數據庫,數據庫操作完成後再寫入redis,然後再把數據返回客户端,修改/刪除的情況下,如果redis有就先更新redis,然後再操作數據庫再返回客户端,如果沒有就先操作數據庫,然後再寫入redis,再返回客户端。這個就是同步,數據都是一條線上操作的,所以最大的保證了數據的一致性,但是處理速度、併發性能肯定就會受到影響。異步的方式:新增的情況下,如果沒有則先寫入redis,然後異步操作數據庫,直接返回成功,如果是查詢的情況下,redis沒有查數據庫後,異步寫入redis,直接返回客户端,如果是修改的情況下,如果redis有就直接改,然後異步操作數據庫,直接返回客户端,如果redis沒有的情況下,就操作數據庫,然後異步操作redis,直接返回客户端,刪除以此類推。異步的方式雖然可以讓請求更快,但是容易出現異步操作的問題,這個時候就需要消息隊列等操作進行異步重試,這種方式一遍是對數據一致性不高的情況下才使用,不然可能因為數據庫的約束導致數據根本無法更新的情況。
緩存擊穿、緩存穿透、緩存雪崩
緩存擊穿:是指某個熱點數據在緩存失效的瞬間,大量併發請求同時訪問該數據,導致所有請求打到數據庫。
緩存穿透:指查詢的數據在緩存和數據庫中都不存在,導致每次請求都直接打到數據庫。
緩存雪崩:是指大量緩存數據在同一時間失效,導致大量請求直接打到數據庫,造成數據庫壓力驟增甚至宕機。
以上這三種情況都可以給我們的數據庫造成毀滅性打擊(當然要數據量上去才行,mysql還是很能打的),同時也對我們緩存雙寫造成了影響。就比如新增一條數據發了100w個請求,而且新增的內容都是一樣的,不管是同步還是異步都會有這個問題,這個時候redis肯定是沒有的,那麼都會去操作數據庫,那麼就會有100w個請求mysql,其實新增操作只有一個,但mysql的性能壓力卻被放大了100w倍,這個是十分嚴重的問題,而且,同步方式操作完數據庫後又會重複更改redis,這也會導致redis壓力增加。異步倒是沒有這種情況,因為異步是先更新reids的,而且redis讀寫線程是原子的、阻塞的。
雙檢加鎖
那為了解決這個問題我們可以採用雙檢加鎖的方法來防止上面提到的問題,保證redis和mysql不會因為大量請求而導致的壓力過大。
這個雙檢就是指的就是檢查兩次,這個加鎖指的就是對數據庫操作加鎖,還是以我們的新增為例子,假設100w條請求同時訪問,那麼第一遍查詢redis都沒有,然後需要操作數據庫,但是不同的就是這個數據庫是上鎖的,不管你多少個人肯定有一個人先拿到,再操作數據庫之前再檢查一遍redis如果有就直接返回了,沒有再操作數據庫,操作數據庫完成後寫入redis,釋放這個鎖。等到下一個人持有這個鎖的時候,先查redis發現有了,直接返回了同時釋放了這個鎖。
這樣就解決了緩存雙寫的問題,而且對於處理的時間並沒有慢多少,最耗時間的就是操作數據庫,但是這個操作只有一次,所以還是毫秒級的,性能影響不大。
緩存雙寫數據一致性策略
先更新數據庫,再更新緩存:一般情況下我們都是以數據庫的數據為準,redis為輔的,但是這種策略並不能保證redis的一致性,比如我們的多線程情況下,大家都知道我們的cpu是一個輪詢操作的,所以並不能保證前後順序,假如A修改了數據庫值為100成功了,這個時候B修改數據為80,然後B更新了redis80,A再更新redis100,那麼這個數據就是錯的。
先更新redis,再更新數據庫:假如A修改了redis為100,B修改redis為80,B修改數據庫80,A修改數據庫為100,那麼這個時候數據也是錯的。redis數據是80是錯誤的。
先刪除緩存,再更新數據庫:假如A刪除了rediskey,然後B請求了key發現沒有去mysql查,然後A修改數據庫,B更新redis,這個時候數據也是錯的,還是redis舊數據
我們可以使用延遲雙刪這個機制來預防這種情況,其實很好理解就是刪除2次,A先刪除一次,然後操作數據庫成功後,等待一會,然後再刪除一次,這樣就能保證數據的正確了,這個等待誰呢?就是等待B線程寫入redis,因為這個B寫入的是舊數據,所以我們只要把他刪除了,就能保證我們下一次查詢會走數據庫,保證數據的正確性,而且我們的第二次刪除一般都是視野異步的方式,因為這樣不會影響線程的吞吐量。那怎麼知道我們需要延遲多久呢?有幾種辦法,一種就是通過實際測量業務操作的耗時來確定延遲時間,比如我們部署測試庫進行壓力測試,然後得到合理的等待時間,還有就是動態監測的方法,比如我們寫一個定時任務,定時執行監測方法然後寫入到靜態文件或者數據庫中。另外就是我們的第二次刪除可能失敗,那麼我們怎麼保證一定成功呢?要麼使用消息隊列進行持久化並重試,要麼使用看門狗機制。
先更新數據庫,再刪除緩存:假如A在操作數據庫,B讀取了redis中的數據/或者讀取了mysql中的舊數據寫入redis,A操作成功,A刪除redis數據,這樣雖然也有部分數據是錯誤的,也就是在A沒有成功修改刪除的情況下其他線程讀的都是舊數據,但是相對於上三種來説對程序的影響是最小的,因為起碼保證了後續的數據都是正確的。這個就是最終一致性,沒辦法保證redis和mysql的強一致性的,除非我們犧牲吞吐量那麼這個系統就太卡啦也不現實。
canal
實時監聽數據庫變更,並將數據變更事件推送給下游系統,實現數據同步解耦和系統架構優化。就拿我們上面的先更新數據庫,再刪除緩存的方式來説,我們刪除redis不一定成功,所以要麼就是使用消息隊列進行錯誤重試,要麼就回滾事務,讓客户端重試,而我們的canal就是把這一塊功能進行解耦。
canal的就是基於mysql的主從複製架構,偽裝成一個從機,然後監聽主機的binlog文件,如果文件修改了,那麼就把修改的內容推送到其他從機,然後再解析binlog中修改的內容,去刪除redis緩存,然後失敗了就放入消息隊列就行重試,這樣既可以把功能模塊進行解耦也可以保證redis的數據成功刪除,當然canal的功能還有很多,這裏就不多説了,怎麼使用怎麼配置也不是本篇的內容了,想知道的小夥伴自行查找吧。
看門狗(Watchdog)
看門狗是一種可靠性保障機制,通過監控、重試、超時控制等手段,確保關鍵操作的執行成功。他其實和canal有點類似,區別就是看門狗是隻看任務執行成功沒有,而canal只看binlog更新沒有,看門狗用到的地方不止延遲雙刪,而且把延遲雙刪的第一次刪除去掉,不就是我們的第4種雙寫方案,也就是説延遲雙刪也可以使用cana,下面是適用場景對比:
|
場景
|
看門狗更適合 |
Canal更適合 |
理由 |
|
簡單業務 |
✅ 推薦 |
❌ 過度設計 |
架構簡單,快速實現 |
|
大型系統 |
⚠️ 可用的 |
✅ 推薦 |
解耦架構,易於維護 |
|
高一致性要求 |
⚠️ 需要複雜實現 |
✅ 天然支持 |
基於事務日誌 |
|
實時性要求高 |
✅ 延遲低 |
⚠️ 有解析延遲 |
直接操作更快 |
|
技術團隊強 |
✅ 都可選 |
✅ 更推薦 |
長期收益大 |
|
現有系統改造 |
✅ 侵入小 |
⚠️ 改造大 |
逐步遷移容易 |
這個是看門狗的執行機制:
總結
本篇主要針對緩存雙寫的解釋以及緩存雙寫的策略和方法進行介紹。