快取和資料庫一致性問題分析

2021-10-05 08:17:43 字數 3866 閱讀 1517

問題:如何保證快取和資料庫雙寫的一致性

一般來說,如果允許快取可以稍微的跟資料庫偶爾有不一致的情況,也就是說如果你的系統不是嚴格要求「快取+資料庫」 必須保持一致性的話,最好不要做這個方案,即:讀請求和寫請求序列化,串到乙個記憶體佇列裡去。

序列化可以保證一定不會出現不一致的情況,但是它也會導致系統的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的乙個請求。

最經典的快取+資料庫讀寫的模式,就是 cache aside pattern。

為什麼是刪除快取,而不是更新快取?

原因很簡單,很多時候,在複雜點的快取場景,快取不單單是資料庫中直接取出來的值。

比如可能更新了某個表的乙個字段,然後其對應的快取,是需要查詢另外兩個表的資料並進行運算,才能計算出快取最新的值的。

另外更新快取的代價有時候是很高的。是不是說,每次修改資料庫的時候,都一定要將其對應的快取更新乙份?也許有的場景是這樣,但是對於比較複雜的快取資料計算的場景,就不是這樣了。如果你頻繁修改乙個快取涉及的多個表,快取也頻繁更新。但是問題在於,這個快取到底會不會被頻繁訪問到?

舉個栗子,乙個快取涉及的表的字段,在 1 分鐘內就修改了 20 次,或者是 100 次,那麼快取更新 20 次、100 次;但是這個快取在 1 分鐘內只被讀取了 1 次,有大量的冷資料。實際上,如果你只是刪除快取的話,那麼在 1 分鐘內,這個快取不過就重新計算一次而已,開銷大幅度降低。用到快取才去算快取。

其實刪除快取,而不是更新快取,就是乙個 lazy 計算的思想,不要每次都重新做複雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。像 mybatis,hibernate,都有懶載入思想。查詢乙個部門,部門帶了乙個員工的 list,沒有必要說每次查詢部門,都把裡面的 1000 個員工的資料也同時查出來啊。80% 的情況,查這個部門,就只是要訪問這個部門的資訊就可以了。先查部門,同時要訪問裡面的員工,那麼這個時候只有在你要訪問裡面的員工的時候,才會去資料庫裡面查詢 1000 個員工。

問題:先更新資料庫,再刪除快取。如果刪除快取失敗了,那麼會導致資料庫中是新資料,快取中是舊資料,資料就出現了不一致。

解決思路:先刪除快取,再更新資料庫。如果資料庫更新失敗了,那麼資料庫中是舊資料,快取中是空的,那麼資料不會不一致。因為讀的時候快取沒有,所以去讀了資料庫中的舊資料,然後更新到快取中。

資料發生了變更,先刪除了快取,然後要去修改資料庫,此時還沒修改。乙個請求過來,去讀快取,發現快取空了,去查詢資料庫,查到了修改前的舊資料,放到了快取中。隨後資料變更的程式完成了資料庫的修改。完了,資料庫和快取中的資料不一樣了...

為什麼上億流量高併發場景下,快取會出現這個問題?

只有在對乙個資料在併發的進行讀寫的時候,才可能會出現這種問題。其實如果說你的併發量很低的話,特別是讀併發很低,每天訪問量就 1 萬次,那麼很少的情況下,會出現剛才描述的那種不一致的場景。但是問題是,如果每天的是上億的流量,每秒併發讀是幾萬,每秒只要有資料更新的請求,就可能會出現上述的資料庫+快取不一致的情況

解決方案如下:

更新資料的時候,根據資料的唯一標識,將操作路由之後,傳送到乙個 jvm 內部佇列中。讀取資料的時候,如果發現資料不在快取中,那麼將重新執行「讀取資料+更新快取」的操作,根據唯一標識路由之後,也傳送到同乙個 jvm 內部佇列中。

乙個佇列對應乙個工作執行緒,每個工作執行緒序列拿到對應的操作,然後一條一條的執行。這樣的話,乙個資料變更的操作,先刪除快取,然後再去更新資料庫,但是還沒完成更新。此時如果乙個讀請求過來,沒有讀到快取,那麼可以先將快取更新的請求傳送到佇列中,此時會在佇列中積壓,然後同步等待快取更新完成。

這裡有乙個優化點,乙個佇列中,其實多個更新快取請求串在一起是沒意義的,因此可以做過濾,如果發現佇列中已經有乙個更新快取的請求了,那麼就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可。

如果請求還在等待時間範圍內,不斷輪詢發現可以取到值了,那麼就直接返回;如果請求等待的時間超過一定時長,那麼這一次直接從資料庫中讀取當前的舊值。

高併發的場景下,該解決方案要注意的問題:

由於讀請求進行了非常輕度的非同步化,所以一定要注意讀超時的問題,每個讀請求必須在超時時間範圍內返回。

該解決方案,最大的風險點在於說,可能資料更新很頻繁,導致佇列中積壓了大量更新操作在裡面,然後讀請求會發生大量的超時,最後導致大量的請求直接走資料庫。務必通過一些模擬真實的測試,看看更新資料的頻率是怎樣的。

另外一點,因為乙個佇列中,可能會積壓針對多個資料項的更新操作,因此需要根據自己的業務情況進行測試,可能需要部署多個服務,每個服務分攤一些資料的更新操作。如果乙個記憶體佇列裡居然會擠壓 100 個商品的庫存修改操作,每個庫存修改操作要耗費 10ms 去完成,那麼最後乙個商品的讀請求,可能等待 10 * 100 = 1000ms = 1s 後,才能得到資料,這個時候就導致讀請求的長時阻塞

一定要做根據實際業務系統的運**況,去進行一些壓力測試,和模擬線上環境,去看看最繁忙的時候,記憶體佇列可能會擠壓多少更新操作,可能會導致最後乙個更新操作對應的讀請求,會 hang 多少時間,如果讀請求在 200ms 返回,如果你計算過後,哪怕是最繁忙的時候,積壓 10 個更新操作,最多等待 200ms,那還可以的。

如果乙個記憶體佇列中可能積壓的更新操作特別多,那麼你就要加機器,讓每個機器上部署的服務例項處理更少的資料,那麼每個記憶體佇列中積壓的更新操作就會越少。

其實根據之前的專案經驗,一般來說,資料的寫頻率是很低的,因此實際上正常來說,在佇列中積壓的更新操作應該是很少的。像這種針對讀高併發、讀快取架構的專案,一般來說寫請求是非常少的,每秒的 qps 能到幾百就不錯了。

我們來實際粗略測算一下

如果一秒有 500 的寫操作,如果分成 5 個時間片,每 200ms 就 100 個寫操作,放到 20 個記憶體佇列中,每個記憶體佇列,可能就積壓 5 個寫操作。每個寫操作效能測試後,一般是在 20ms 左右就完成,那麼針對每個記憶體佇列的資料的讀請求,也就最多 hang 一會兒,200ms 以內肯定能返回了。

經過剛才簡單的測算,我們知道,單機支撐的寫 qps 在幾百是沒問題的,如果寫 qps 擴大了 10 倍,那麼就擴容機器,擴容 10 倍的機器,每個機器 20 個佇列。

這裡還必須做好壓力測試,確保恰巧碰上上述情況的時候,還有乙個風險,就是突然間大量讀請求會在幾十毫秒的延時 hang 在服務上,看服務能不能扛的住,需要多少機器才能扛住最大的極限情況的峰值。

但是因為並不是所有的資料都在同一時間更新,快取也不會同一時間失效,所以每次可能也就是少數資料的快取失效了,然後那些資料對應的讀請求過來,併發量應該也不會特別大。

可能這個服務部署了多個例項,那麼必須保證說,執行資料更新操作,以及執行快取更新操作的請求,都通過 nginx 伺服器路由到相同的服務例項上

比如說,對同乙個商品的讀寫請求,全部路由到同一臺機器上。可以自己去做服務間的按照某個請求引數的 hash 路由,也可以用 nginx 的 hash 路由功能等等。

萬一某個商品的讀寫請求特別高,全部打到相同的機器的相同的佇列裡面去了,可能會造成某台機器的壓力過大。就是說,因為只有在商品資料更新的時候才會清空快取,然後才會導致讀寫併發,所以其實要根據業務系統去看,如果更新頻率不是太高的話,這個問題的影響並不是特別大,但是的確可能某些機器的負載會高一些。

Redis和資料庫快取一致性問題之我見

乙個經典的問題,redis經常被用來當作快取,那麼redis快取一致性怎麼解決?翻閱了網上很多資料,答案不一,這裡簡單整理一下我的看法。目錄2 先運算元據庫,後操作快取 2.2 先更新資料庫,再刪快取 3 個人總結 4 番外 從借鑑作業系統的一些方法 參考問題 髒寫 在併發的情況下,可能出現以下情況...

快取與資料庫一致性問題

業務場景 抓拍到的人臉需要推送到第三方系統,但不是所有的網點都需要推送資訊。也就是要做到不同的網點可以根據配置來決定是否推送,前端頁面需要有推送配置功能,手動配置後,把配置的推送資訊儲存到資料庫。抓拍到人臉 後,讀取配置的推送資訊,再判斷是否需要推送。由於網點多抓拍的人臉資料量較大,推送資訊配置後不...

資料庫和快取一致性問題

前言 快取一致性是指業務在引入分布式快取系統後,業務對資料的更新除了要更新儲存以外還需要同時更新快取,對兩個系統進行資料更新就要先解決分布式系統中的隔離性和原子性難題。目前大多數業務在引入分布式快取後都是通過犧牲小概率的一致性來保障業務效能,因為要在業務層嚴格保障資料的一致性,代價非常高,業務引入分...