02 youtube官網打不開如何清除緩存數據一致性是指(經典好文--如何保證緩存和數據庫的雙寫一致性)

时间:2024-05-20 00:14:50 编辑: 来源:

份,也在緩存中保存一份。

對于一致性來說,包含強一致性和弱一致性 ,強一致性保證寫入后立即可以讀取,弱一致性則不保證立即可以讀取寫入后的值,而是盡可能的保證在經過一定時間后可以讀取到,在弱一致性中應用最為廣泛的模型則是最終一致性模型,即保證在一定時間之后寫入和讀取達到一致的狀態。對于應用緩存的大部分場景來說,追求的則是最終一致性,少部分對數據一致性要求極高的場景則會追求強一致性。

為了達到最終一致性,針對不同的場景,業界逐步形成了下面這幾種應用緩存的策略。

— 1 —

Cache-Aside

Cache-Aside 意為旁路緩存模式,是應用最為廣泛的一種緩存策略。下面的圖示展示了它的讀寫流程,來看看它是如何保證最終一致性的。在讀請求中,首先請求緩存,若緩存命中(cache hit),則直接返回緩存中的數據;若緩存未命中(cache miss),則查詢數據庫并將查詢結果更新至緩存,然后返回查詢出的數據(demand-filled look-aside )。在寫請求中,先更新數據庫,再刪除緩存(write-invalidate)。

1、為什么刪除緩存,而不是更新緩存?

在 Cache-Aside 中,對于讀請求的處理比較容易理解,但在寫請求中,可能會有讀者提出疑問,為什么要刪除緩存,而不是更新緩存?站在符合直覺的角度來看,更新緩存是一個容易被理解的方案,但站在性能和安全的角度,更新緩存則可能會導致一些不好的后果。

首先是性能 ,當該緩存對應的結果需要消耗大量的計算過程才能得到時,比如需要訪問多張數據庫表并聯合計算,那么在寫操作中更新緩存的動作將會是一筆不小的開銷。同時,當寫操作較多時,可能也會存在剛更新的緩存還沒有被讀取到,又再次被更新的情況(這常被稱為緩存擾動),顯然,這樣的更新是白白消耗機器性能的,會導致緩存利用率不高。

而等到讀請求未命中緩存時再去更新,也符合懶加載的思路,需要時再進行計算。刪除緩存的操作不僅是冪等的,可以在發生異常時重試,而且寫-刪除和讀-更新在語義上更加對稱。

其次是安全 ,在并發場景下,在寫請求中更新緩存可能會引發數據的不一致問題。參考下面的圖示,若存在兩個來自不同線程的寫請求,首先來自線程 1 的寫請求更新了數據庫(step 1),接著來自線程 2 的寫請求再次更新了數據庫(step 3),但由于網絡延遲等原因,線程 1 可能會晚于線程 2 更新緩存(step 4 晚于 step 3),那么這樣便會導致最終寫入數據庫的結果是來自線程 2 的新值,寫入緩存的結果是來自線程 1 的舊值,即緩存落后于數據庫,此時再有讀請求命中緩存(step 5),讀取到的便是舊值。

2、為什么先更新數據庫,而不是先刪除緩存?

另外,有讀者也會對更新數據庫和刪除緩存的時序產生疑問,那么為什么不先刪除緩存,再更新數據庫呢?在單線程下,這種方案看似具有一定合理性,這種合理性體現在刪除緩存成功。

但更新數據庫失敗的場景下,盡管緩存被刪除了,下次讀操作時,仍能將正確的數據寫回緩存,相對于 Cache-Aside 中更新數據庫成功,刪除緩存失敗的場景來說,先刪除緩存的方案似乎更合理一些。那么,先刪除緩存有什么問題呢?

問題仍然出現在并發場景下,首先來自線程 1 的寫請求刪除了緩存(step 1),接著來自線程 2 的讀請求由于緩存的刪除導致緩存未命中,根據 Cache-Aside 模式,線程 2 繼而查詢數據庫(step 2),但由于寫請求通常慢于讀請求,線程 1 更新數據庫的操作可能會晚于線程 2 查詢數據庫后更新緩存的操作(step 4 晚于 step 3),那么這樣便會導致最終寫入緩存的結果是來自線程 2 中查詢到的舊值,而寫入數據庫的結果是來自線程 1 的新值,即緩存落后于數據庫,此時再有讀請求命中緩存( step 5 ),讀取到的便是舊值。

另外,先刪除緩存,由于緩存中數據缺失,加劇數據庫的請求壓力,可能會增大緩存穿透出現的概率。

3、如果選擇先刪除緩存,再更新數據庫,那如何解決一致性問題呢?

為了避免“先刪除緩存,再更新數據庫”這一方案在讀寫并發時可能帶來的緩存臟數據,業界又提出了延時雙刪的策略,即在更新數據庫之后,延遲一段時間再次刪除緩存,為了保證第二次刪除緩存的時間點在讀請求更新緩存之后,這個延遲時間的經驗值通常應稍大于業務中讀請求的耗時。

延遲的實現可以在代碼中 sleep 或采用延遲隊列。顯而易見的是,無論這個值如何預估,都很難和讀請求的完成時間點準確銜接,這也是延時雙刪被詬病的主要原因。

4、那么 Cache-Aside 存在數據不一致的可能嗎?

在 Cache-Aside 中,也存在數據不一致的可能性。在下面的讀寫并發場景下,首先來自線程 1 的讀請求在未命中緩存的情況下查詢數據庫(step 1),接著來自線程 2 的寫請求更新數據庫(step 2),但由于一些極端原因,線程 1 中讀請求的更新緩存操作晚于線程 2 中寫請求的刪除緩存的操作(step 4 晚于 step 3),那么這樣便會導致最終寫入緩存中的是來自線程 1 的舊值,而寫入數據庫中的是來自線程 2 的新值,即緩存落后于數據庫,此時再有讀請求命中緩存(step 5),讀取到的便是舊值。

這種場景的出現,不僅需要緩存失效且讀寫并發執行,而且還需要讀請求查詢數據庫的執行早于寫請求更新數據庫,同時讀請求的執行完成晚于寫請求。足以見得,這種 不一致場景產生的條件非常嚴格,在實際的生產中出現的可能性較小 。

除此之外,在并發環境下,Cache-Aside 中也存在讀請求命中緩存的時間點在寫請求更新數據庫之后,刪除緩存之前,這樣也會導致讀請求查詢到的緩存落后于數據庫的情況。

雖然在下一次讀請求中,緩存會被更新,但如果業務層面對這種情況的容忍度較低,那么可以采用加鎖在寫請求中保證“更新數據庫&刪除緩存”的串行執行為原子性操作(同理也可對讀請求中緩存的更新加鎖)。 加鎖勢必會導致吞吐量的下降,故采取加鎖的方案應該對性能的損耗有所預期。

— 2 —

補償機制

我們在上面提到了,在 Cache-Aside 中可能存在更新數據庫成功,但刪除緩存失敗的場景,如果發生這種情況,那么便會導致緩存中的數據落后于數據庫,產生數據的不一致的問題。

其實,不僅 Cache-Aside 存在這樣的問題,在延時雙刪等策略中也存在這樣的問題。針對可能出現的刪除失敗問題,目前業界主要有以下幾種補償機制。

1、刪除重試機制

由于同步重試刪除在性能上會影響吞吐量,所以常通過引入消息隊列,將刪除失敗的緩存對應的 key 放入消息隊列中,在對應的消費者中獲取刪除失敗的 key ,異步重試刪除。這種方法在實現上相對簡單,但由于刪除失敗后的邏輯需要基于業務代碼的 trigger 來觸發 ,對業務代碼具有一定入侵性。

鑒于上述方案對業務代碼具有一定入侵性,所以需要一種更加優雅的解決方案,讓緩存刪除失敗的補償機制運行在背后,盡量少的耦合于業務代碼。一個簡單的思路是通過后臺任務使用更新時間戳或者版本作為對比獲取數據庫的增量數據更新至緩存中,這種方式在小規模數據的場景可以起到一定作用,但其擴展性、穩定性都有所欠缺。

一個相對成熟的方案是基于 MySQL 數據庫增量日志進行解析和消費,這里較為流行的是阿里巴巴開源的作為 MySQL binlog 增量獲取和解析的組件 canal(類似的開源組件還有 Maxwell、Databus 等)。

canal sever 模擬 MySQL slave 的交互協議,偽裝為 MySQL slave,向 MySQL master 發送 mp 協議,MySQL master 收到 mp 請求,開始推送 binary log 給 slave (即 canal sever ),canal sever 解析 binary log 對象(原始為 byte 流),可由 canal client 拉取進行消費,同時 canal server 也默認支持將變更記錄投遞到 MQ 系統中,主動推送給其他系統進行消費。

在 ack 機制的加持下,不管是推送還是拉取,都可以有效的保證數據按照預期被消費。當前版本的 canal 支持的 MQ 有 Kafka 或者 RocketMQ。另外, canal 依賴 ZooKeeper 作為分布式協調組件來實現 HA ,canal 的 HA 分為兩個部分:

那么,針對緩存的刪除操作便可以在 canal client 或 買粉絲nsumer 中編寫相關業務代碼來完成。這樣,結合數據庫日志增量解析消費的方案以及 Cache-Aside 模型,在讀請求中未命中緩存時更新緩存(通常這里會涉及到復雜的業務邏輯),在寫請求更新數據庫后刪除緩存,并基于日志增量解析來補償數據庫更新時可能的緩存刪除失敗問題,在絕大多數場景下,可以有效的保證緩存的最終一致性。

另外需要注意的是,還應該隔離事務與緩存,確保數據庫入庫后再進行緩存的刪除操作。 比如考慮到數據庫的主從架構,主從同步及讀從寫主的場景下,可能會造成讀取到從庫的舊數據后便更新了緩存,導致緩存落后于數據庫的問題,這就要求對緩存的刪除應該確保在數據庫操作完成之后。所以,基于 binlog 增量日志進行數據同步的方案,可以通過選擇解析從節點的 binlog,來避免主從同步下刪除緩存過早的問題。

3、數據傳輸服務 DTS

— 3 —

Read-Through

Read-Through 意為讀穿透模式,它的流程和 Cache-Aside 類似,不同點在于 Read-Through 中多了一個訪問控制層,讀請求只和該訪問控制層進行交互,而背后緩存命中與否的邏輯則由訪問控制層與數據源進行交互,業務層的實現會更加簡潔,并且對于緩存層及持久化層交互的封裝程度更高,更易于移植。

搜索关键词: