1、三種常用的快取模式
1.旁路快取模式
一般來說,如果允許快取可以稍微的跟資料庫偶爾有不一致的情況,也就是說如果你的系統不是嚴格要求 “快取+資料庫” 必須保持一致性的話,最好不要做這個方案,即:讀請求和寫請求串行化,串到一個記憶體佇列里去,
采用快取 + 資料庫讀寫的方式,就是 Cache Aside Pattern(旁路快取模式),
- 讀的時候,先讀快取,快取沒有的話,就讀資料庫,然后取出資料后放入快取,同時回傳回應,
- 更新的時候,先更新資料庫,然后再洗掉快取,
2.讀寫穿透模式
Read/Write Through Pattern 中服務端把 cache 視為主要資料存盤,從中讀取資料并將資料寫入其中,cache 服務負責將此資料讀取和寫入 db,從而減輕了應用程式的職責,
寫(Write Through):先查 cache,cache 中不存在,直接更新 db;cache 中存在,則先更新 cache,然后 cache 服務自己更新 db(同步更新 cache 和 db)
讀(Read Through):從 cache 中讀取資料,讀取到就直接回傳 ;讀取不到的話,先從 db 加載,寫入到 cache 后回傳回應,
Read-Through Pattern 實際只是在 Cache-Aside Pattern 之上進行了封裝,在 Cache-Aside Pattern 下,發生讀請求的時候,如果 cache 中不存在對應的資料,是由客戶端自己負責把資料寫入 cache,而 Read Through Pattern 則是 cache 服務自己來寫入快取的,這對客戶端是透明的,和 Cache Aside Pattern 一樣, Read-Through Pattern 也有首次請求資料一定不再 cache 的問題,對于熱點資料可以提前放入快取中,
3.異步快取寫入
異步快取寫入(Write Behind Pattern) 和 Read/Write Through Pattern 很相似,兩者都是由 cache 服務來負責 cache 和 db 的讀寫,
但是,兩個又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 則是只更新快取,不直接更新 db,而是改為異步批量的方式來更新 db,
很明顯,這種方式對資料一致性帶來了更大的挑戰,比如 cache 資料可能還沒異步更新 db 的話,cache 服務可能就就掛掉了,
這種策略在我們平時開發程序中也非常非常少見,但是不代表它的應用場景少,比如訊息佇列中訊息的異步寫入磁盤、MySQL 的 Innodb Buffer Pool 機制都用到了這種策略,
Write Behind Pattern 下 db 的寫性能非常高,非常適合一些資料經常變化又對資料一致性要求沒那么高的場景,比如瀏覽量、點贊量,
2、快取存在的問題?
1.為什么先更新后洗掉?
結論:無論先洗掉還是先更新資料庫都存在資料一致性問題,那么矮個子里選將軍,選個發生問題概率小的,就是先更新資料庫后洗掉快取,
先洗掉快取,再更新資料庫:如果洗掉快取失敗了,那么會導致資料庫中是新資料,快取中是舊資料,資料就出現了不一致,
2 個執行緒要并發「讀寫」資料,可能會發生以下場景:
- 執行緒 A 要更新 X = 2(原值 X = 1)
- 執行緒 A 先洗掉快取
- 執行緒 B 讀快取,發現不存在,從資料庫中讀取到舊值(X = 1)
- 執行緒 A 將新值寫入資料庫(X = 2)
- 執行緒 B 將舊值寫入快取(X = 1)
最終 X 的值在快取中是 1(舊值),在資料庫中是 2(新值),發生不一致,
先更新資料庫,再洗掉快取:先洗掉了快取,然后要去修改資料庫,此時還沒修改,一個請求過來,去讀快取,發現快取空了,去查詢資料庫,查到了修改前的舊資料,并將其放到了快取中,隨后資料變更的程式完成了資料庫的修改,資料庫和快取中的資料不一樣了
- 快取中 X 不存在(資料庫 X = 1)
- 執行緒 A 讀取資料庫,得到舊值(X = 1)
- 執行緒 B 更新資料庫(X = 2)
- 執行緒 B 洗掉快取
- 執行緒 A 將舊值寫入快取(X = 1)
最終 X 的值在快取中是 1(舊值),在主從庫中是 2(新值),也發生不一致,
這 2 個問題的核心在于:快取都被回種了「舊值」,
矮個子里選將軍
第二種方法其實概率很低,這是因為它必須滿足 3 個條件:
- 快取剛好已失效
- 讀請求 + 寫請求并發
- 更新資料庫 + 洗掉快取的時間(步驟 3-4),要比讀資料庫 + 寫快取時間短(步驟 2 和 5)
仔細想一下,條件 3 發生的概率其實是非常低的,
因為寫資料庫一般會先「加鎖」,所以更新資料庫,通常是要比讀資料庫的時間更長的,并且因為快取的寫入速度是比資料庫的寫入速度快很多,這么來看,「先更新資料庫 + 再洗掉快取」的方案,是可以保證資料一致性的,所以,我們應該采用這種方案,來操作資料庫和快取,
2.解決方法
最有效的辦法就是,把快取刪掉,但是,不能立即刪,而是需要「延遲刪」,即:快取延遲雙刪策略,
解決第一個問題:在執行緒 A 洗掉快取、更新完資料庫之后,先「休眠一會」,再「洗掉」一次快取,
解決第二個問題:執行緒 A 可以生成一條「延時訊息」,寫到訊息佇列中,消費者延時「洗掉」快取,
這兩個方案的目的,都是為了把快取清掉,這樣一來,下次就可以從資料庫讀取到最新值,寫入快取,
3.如何保證洗掉快取成功?
方案一:重試
首先想到的一個方案是:執行失敗后,重試,失敗后立即重試的問題在于:
- 立即重試很大概率「還會失敗」
- 「重試次數」設定多少才合理?
- 重試會一直「占用」這個執行緒資源,無法服務其它客戶端請求
方案二:異步重試
異步重試其實就是:把重試請求扔到「訊息佇列」中,然后由專門的消費者來重試,直到成功,把重試或第二步操作放到另一個服務中,這個服務用訊息佇列來進行重試操作,
3、異步重試方案-canal
我們的業務應用在修改資料時,「只需」修改資料庫,無需操作快取,拿 MySQL 舉例,當一條資料發生修改時,MySQL 就會產生一條變更日志(Binlog),我們可以訂閱這個日志,拿到具體操作的資料,然后再根據這條資料,去洗掉對應的快取,訂閱變更日志,目前也有了比較成熟的開源中間件,例如阿里的 canal,使用這種方案的優點在于:
- 無需考慮寫訊息佇列失敗情況:只要寫 MySQL 成功,Binlog 肯定會有
- 自動投遞到下游佇列:canal 自動把資料庫變更日志「投遞」給下游的訊息佇列
想要保證資料庫和快取一致性,推薦采用「先更新資料庫,再洗掉快取」方案,并配合「訊息佇列」或「訂閱變更日志」的方式來做,
參考: https://www.cnblogs.com/myseries/p/12068845.html
3種常用的快取讀寫策略詳解
快取和資料庫一致性問題,看這篇就夠了 - 水滴與銀彈
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/546757.html
標籤:NoSQL
