摘要:行存表示了一種數據的存儲方式,是最傳統的一種存儲方式。 本文分享自華為雲社區《【玩轉PB級數倉GaussDB(DWS)】行列存對比的一些事》,作者:sevenjiang。 行存表示了一種數據的存儲方式,是最傳統的一種存儲方式。對於GaussDB(DWS)來說可以認為其表示存儲引擎的基礎實現,在 ...
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