Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 叢集模式
追求效能極致:Redis6.0的多執行緒模型
追求效能極致:使用者端快取帶來的革命
Redis系列8:Bitmap實現億萬級資料計算
Redis系列9:Geo 型別賦能億級地圖位置計算
Redis系列10:HyperLogLog實現海量資料基數統計
Redis系列11:記憶體淘汰策略
Redis系列12:Redis 的事務機制
Redis系列13:分散式鎖實現
Redis系列14:使用List實現訊息佇列
Redis系列15:使用Stream實現訊息佇列
Redis系列16:聊聊布隆過濾器(原理篇)
Redis系列17:聊聊布隆過濾器(實踐篇)
Redis系列18:過期資料的刪除策略
Redis系列19:LRU淘汰記憶體淘汰演演算法分析
Redis系列20:LFU淘汰記憶體淘汰演演算法分析
快取與資料庫的資料一致性指的是,快取中儲存的資料與資料庫中儲存的資料需保持一致。
即快取中存有資料,快取的資料值 = 資料庫中的值;快取中沒有該資料,資料庫中的值 = 最新值。資料一致性主要包含以下兩種情況:
如果存在以下情況,則說明存在不一致性情況:
快取(Redis)和 資料庫(MySQL)是兩套系統,所以任何一方的資料改寫,都需要另一方的協同來保證。但這種協同可能存在一定的失敗率,如下:
為了保持快取和資料庫的資料一致性,需要採取適當的一致性策略(如引入 2PC 或 Paxos 等分散式一致性協定,或者分散式鎖),並及時處理資料庫更新和快取重新整理中的錯誤。同時,在實現並行請求時,需要合理控制操作的順序和時機,以避免不一致的情況發生。
我們先來了解下快取常用的執行策略,再分析下那種策略最適合一致性保障。
快取旁路策略也是目前業務系統最常用的策略。在Cache-Aside機制中,系統將快取視為一個輔助的儲存媒介,所有的讀取快取、讀取資料庫和更新快取的操作都由應用服務來完成。
讀取快取資料的步驟如下:
程式實現如下(go 語言版本虛擬碼):
func main() {
// 嘗試從快取獲取資料
cacheValue := getFromCache("testinfo")
if cacheValue != "" {
return cacheValue
} else {
// 如果快取缺失,則從資料庫獲取資料
cacheValue = getDataFromDB()
// 將資料寫入快取
setInCache(cacheValue)
return cacheValue
}
}
更新資料的步驟如下:
可以看到資料庫更新之後,讓快取與資料庫的同步的方式有兩種,一種是同步去更新快取的value,一種是直接刪除資料庫。
因為高頻模式下,更新夠頻繁,更新執行緒的執行先後可能導致髒資料情況,所以比較常用的方式是刪除快取使快取資料失效來實現同步。具體優勢如下:
程式實現如下(go 語言版本虛擬碼):
func main() {
_, err = db.Save(&user) // 儲存更新後的資料到資料庫
if err != nil {
log.Fatal(err)
} else {
// 更新成功之後,刪除快取
err = deleteCache(id)
}
}
整體優勢:
Cache-Aside機制可以有效地提高系統的效能,因為快取可以減少資料庫等資料來源的存取量,從而減少了系統的響應時間。同時,它還可以提高資料的可靠性和一致性,避免髒資料的出現。
當快取未命中,從資料庫讀取資料,同時寫到快取中並返回給應用服務。
這個策略類似於Cache-Aside,但是它會自動從快取中讀取資料,而不需要先從資料庫中讀取資料。如果快取中沒有資料,則自動從資料庫中讀取資料,並將讀取到的資料儲存到快取中。
值得注意的是,Read-Through 不對資料庫與快取的同步關注,程式碼只與快取互動,由快取元件來管理自身與資料庫之間的資料同步。所以說這個策略雖然可以提高讀取效能,但是可能會增加資料庫的負載。
這個策略類似於Cache-Aside,但是在寫入資料時,Write-Through 將寫入責任轉移到快取系統,由快取服務來執行更新,而不是先寫入資料庫再更新快取。
將同步可能產生的故障處理和重試邏輯,交給快取層來管理實現。
流程圖如下:
這個策略可以提高寫入效能,但是可能會降低快取的利用率。
可以看出與Cache-Aside最大不通就是對調了順序,更新資料時,資料先寫快取,接著由快取元件將資料同步到資料庫。
這個策略類似於Write-Through,但是在寫入資料時,它會非同步地更新快取和資料庫,而不是立即更新。這個策略可以提高寫入效能,但是可能會增加資料庫的負載和快取的不一致性。
流程如下:
這種策略下,快取與資料庫存在一定程度的不一致性,對強要求一致的系統不建議使用。
我們在業務場景中最經常使用的是 Cache-Aside 策略。在該策略下,先從快取中讀取資料,如果快取中沒有資料,則從資料庫中讀取資料,並將讀取到的資料儲存到快取中。在寫入資料時,先寫入資料庫,然後更新快取中的資料。
可以看出,讀操作不會導致快取與資料庫的不一致。而寫操作則存在風險,資料庫和快取畢竟是兩套系統,如果都需要進行修改,它們的先後順序可能導致資料不一致。
上面其實我們有討論過,更新的時候有兩種辦法,一種是將資料庫的修改更新到快取;一種是直接刪除快取,等有需要呼叫的時候再去更新快取。
所以規避一致性風險的時候,我們需要考慮的有:
在對這四種的方案的分析過程中,我們考慮兩個點:
如圖可以看出:如果快取更新成功,資料庫更新失敗,就會導致資料庫和快取的資料不一致,那快取就是髒資料了。
而查詢的時候,會從快取中查詢到資料庫不一致的資料,這樣的資料是不正確的。
如圖可以看出:
綜上,高並行場景中,多執行緒同時寫快取寫資料庫時,很容易出現雙值不一致的情況。
如圖可以看出:
綜上,這種情況也有很大缺陷,不論是異常情況還是高並行場景,都可能導致資料不一致。
如圖可以看出:
可以看出,可能出現短暫的少量讀取舊值的情況,但是很快快取就會被刪除,然後從資料庫獲取最新的值並更新到快取。
之後的請求都能獲取最新資料,這個方案比之前的三種都好很多。
延遲雙刪策略主要是是應對先刪除快取,再更新資料庫的場景。
我們知道在這種場景中,很容易因為刪除資料庫太慢導致重新獲取的快取依舊讀是資料庫舊值,讀完舊值之後,資料庫才更新完畢。這時候快取的資料就跟資料庫不一致了。參考 3.3 節。
所以這邊多加了一個步驟,就是在資料庫更新完成之後,再刪除一次快取。所以步驟如下:
這時候唯一存在的一個問題就是,在(更新據庫 + 休眠 n ms) 這個時間視窗中,依舊能讀取到舊值,而這個短暫時間控制的好的話,是可以接受的。
無論是先更新資料庫,再刪除快取;還是先刪除快取,在更新資料庫。
保持事務性都是一種方案,如果刪除快取失敗,則資料庫更新會被回滾;如果更新資料庫失敗,則快取也不會被刪除。這個需要一致性策略接入。不過無論怎麼做,這個都會在一定程度上影響執行完成的效能。
接著 4.1 的模式,如果雙刪還是失敗呢,那可咋整,還是會產生快取和資料庫資料不一致的現象。
一般的做法是做一層兜底,比如記錄紀錄檔,人工來處理;或者通過MQ來發布訊息,然後開發一個獨立的服務來訂閱,專門用於資料清理,這就將操作非同步化了。
我們知道,資料庫有類似Binlog之類的東西,記錄每次資料的更新。所以我們有另外一種方案,就是把同步快取的操作交給獨立的能力中,從應用層解耦。
圖中我們可以看到步驟如下: