redis基礎篇——資料一致性

石灰聰發表於2020-12-01

資料一致性



快取使用場景


針對讀多寫少的高併發場景,我們可以使用快取來提升查詢速度。
當我們使用Redis作為快取的時候,一般流程是這樣的:

  1. 如果資料在Redis存在,應用就可以直接從Redis拿到資料,不用訪問資料庫。
    在這裡插入圖片描述

  2. 應用新增了資料,只儲存在資料庫中,這個時候Redis沒有這條資料。
    如果Redis裡面沒有,先到資料庫查詢,然後寫入到Redis,再返回給應用。
    在這裡插入圖片描述


一致性問題的定義


因為資料最終是以資料庫為準的(這是我們的原則),如果Redis沒有資料,就不存在這個問題。當Redis和資料庫都有同一條記錄,而這條記錄發生變化的時候,就可能出現一致性的問題。

一旦被快取的資料發生變化(比如修改、刪除)的時候,我們既要運算元據庫的資料,也要操作Redis的資料,才能讓Redis和資料庫保持一致。所以問題來了。現在我們有兩種選擇

  1. 先操作Redis的資料再運算元據庫的資料
  2. 先運算元據庫的資料再操作Redis的資料

到底選哪一種?

首先需要明確的是,不管選擇哪一種方案,我們肯定是希望兩個操作要麼都成功,要麼都一個都不成功。但是,Redis的資料和資料庫的資料是不可能通過事務達到統一的我們只能根據相應的場景和所需要付出的代價來採取一些措施降低資料不一致的問題出現的概率,在資料一致性和效能之間取得一個權衡。

比如,對於資料庫的實時性一致性要求不是特別高的場合,比如T+1的報表,可以採用定時任務查詢資料庫資料同步到Redis的方案。

由於我們是以資料庫的資料為準的,所以給快取設定一個過期時間,刪除Redis的資料,也能保證最終一致性。

我們既然提到了Redis和資料庫一致性的問題,一般是希望儘可能靠近實時一致性,操作延遲帶來的不一致的時間越少越好。


方案選擇


Redis:刪除還是更新?


當儲存的資料發生變化,Redis的資料也要更新的時候,我們有兩種方案,一種就是直接更新Redis資料,呼叫set;還有一種是直接刪除Redis資料,讓應用在下次查詢的時候重新寫入。

這兩種方案怎麼選擇呢?這裡我們主要考慮更新快取的代價。

更新快取之前,是不是要經過其他表的查詢、介面呼叫、計算才能得到最新的資料,而不是直接從資料庫拿到的值。如果是的話,建議直接刪除快取,這種方案更加簡單,而且避免了資料庫的資料和快取不一致的情況。在一般情況下,我們也推薦使用刪除的方案。

所以,更新操作和刪除操作,只要資料變化,都用刪除。
這一點明確之後,現在我們就剩一個問題:

  1. 到底是先更新資料庫,再刪除快取
  2. 還是先刪除快取,再更新資料庫話起

先更新資料庫,再刪除快取


正常情況:

  • 更新資料庫,成功。
  • 刪除快取,成功。

異常情況:

  1. 更新資料庫失敗,程式捕獲異常,不會走到下一步,所以資料不會出現不一致。
  2. 更新資料庫成功,刪除快取失敗。資料庫是新資料,快取是舊資料,發生了不一致的情況。

這種問題怎麼解決呢?我們可以提供一個重試的機制。
比如:如果刪除快取失敗,我們捕獲這個異常,把需要刪除的key傳送到訊息佇列。
然後自己建立一個消費者消費,嘗試再次刪除這個key.


這種方式有個缺點,會對業務程式碼造成入侵。


所以我們又有了第二種方案(非同步更新快取):

因為更新資料庫時會往binlog寫入日誌,所以我們可以通過一個服務來監聽binlog的變化(比如阿里的canal),然後在客戶端完成刪除key的操作。如果刪除失敗的話再傳送到訊息佇列。

總之,對於後刪除快取失敗的情況,我們的做法是不斷地重試刪除,直到成功。
無論是重試還是非同步刪除,都是最終一致性的思想。


先刪除快取,再更新資料庫


正常情況:

  • 刪除快取,成功。
  • 更新資料庫,成功。

異常情況:

  • 刪除快取,程式捕獲異常,不會走到下一步,所以資料不會出現不一致。
  • 刪除快取成功,更新資料庫失敗。因為以資料庫的資料為準,所以不存在資料不一致的情況。

看起來好像沒問題,但是如果有程式併發操作的情況下:

1)執行緒A需要更新資料,首先刪除了Redis快取
2)執行緒B查詢資料,發現快取不存在,到資料庫查詢舊值,寫入Redis,返回
3)執行緒A更新了資料庫

這個時候,Redis是舊的值,資料庫是新的值,發生了資料不一致的情況。

這個是由於執行緒併發造成的問題。能不能讓對同一條資料的訪問序列化呢?
程式碼肯定保證不了,因為有多個執行緒,即使做了任務佇列也可能有多個應用例項(應用做了叢集部署)。
資料庫也保證不了,因為會有多個資料庫的連線。只有一個資料庫只提供一個連線的情況下,才能保證讀寫的操作是序列的,或者我們把所有的讀寫請求放到同一個記憶體佇列當中,但是強制序列操作,吞吐量太低了。

怎麼辦呢?刪一次不放心,隔一段時間再刪一次。
所以我們有一種延時雙刪的策略,在寫入資料之後,再刪除一次快取。

A執行緒:
1)刪除快取
2)更新資料庫
3)休眠500ms(這時間,依據讀取資料的耗時而定)
4)再次刪除快取

相關文章