快取與資料庫一致性

_吹雪_發表於2018-10-15

1.使用快取的場景

快取是提高系統讀效能的常用技術,尤其對於讀多寫少的應用場景,使用快取可以極大的提高系統的效能.

例子:查詢使用者的存款: select money from user where uid = YYY;為了優化該查詢功能,我們可以在快取中建立uid->money的鍵值對。減少資料庫的查詢壓力。

2.讀操作流程

目前資料庫和快取中都有儲存資料,當讀取資料的時候,流程如下。

  1. 先讀取快取是否存在資料(uid->money)。如果快取中有資料返回結果。

  2. 如果快取中沒有資料,則從資料庫中讀取資料。

介紹一個概念:快取命中率:快取命中數/總快取訪問數。

3. 寫操作流程

在介紹寫操作流程之前,先討論兩個問題

問題一:淘汰快取還是更新快取?

淘汰快取:資料只會寫入資料庫,不會寫入快取,只會把資料淘汰掉。

更新快取:資料不但寫入資料庫,還會寫入快取。

問題二:先寫快取還是先寫資料庫?

由於對快取的更新和資料庫的更新無法保證事務性操作。一定涉及到哪個先做,哪個後做的問題,我們的原則是採取對業務影響小的策略。下面是四種不同的組合策略:
http://static.oschina.net/uploads/space/2016/0409/104640_WcMR_818912.png

由此可見第四種策略的影響最小,只會造成一次查詢快取miss而已。那麼當查詢快取miss的時候,我們該怎麼辦?很簡單,查詢資料庫,然後將資料庫的內容更新到快取中。可能有人會問第四種策略,如果一上來淘汰快取就失敗了怎麼辦,當然是直接返回即可,通知使用者本次操作失敗。

我們的結論是:先淘汰快取,再寫資料庫。

4. 分散式環境下如何保證一致性

下面我們再簡單回顧下”先淘汰快取,再寫資料庫 ”策略的讀寫流程。

寫流程:

  1. 先淘汰快取
  2. 再寫資料庫

讀流程:

  1. 先讀快取,如果資料命中則返回
  2. 如果資料未命中則讀取資料庫
  3. 將資料庫讀出來的資料寫入快取

4.1 不一致性的例子

我們的這種策略在序列執行的情況,保證一致性是沒有問題的。但是在分散式環境下,資料的讀寫都是併發的,可能有多個服務對同一個資料進行讀寫,也就是說後發出來的請求有可能先完成。我們來舉個例子:
http://static.oschina.net/uploads/space/2016/0409/104735_I5Zm_818912.png

  1. 傳送了寫請求A,A的第一步淘汰了cache(如上圖中的1)
  2. A的第二步寫資料庫,發出修改請求(如上圖中的2)
  3. 傳送了讀請求B,B的第一步讀取cache, 發現cache中是空的(如上圖中的3)
  4. B的第二步讀取資料庫,發出讀取請求,此時A的第二步寫資料還沒完成,讀出了髒資料,並放入了cache(如上圖中的4)。即後發出的請求4比先發出的請求2先完成了,讀出了髒資料,髒資料又入了快取,造成快取與資料庫中的資料不一致。

4.2解決思路

我們來仔細看一下上面的例子,其實問題就出在對同一資料讀取/寫入請求不是序列的,而是併發的。那麼如何能做到對同一資料的讀取/寫入請求是序列的?只需要讓”同一資料的訪問通過同一條DB連線執行 ”就行。如何做到這一點?可以修改獲取DB連線的方法CPool.DBConnection(), 修改為CPool.DBConnection(uuid)[返回uuid取模相關聯的連線]。

等等,”CPool.DBConnection(uuid)”這個程式碼是執行在每個service上面的,這樣只能保證每個service上面是同一條DB連線。如何解決這個問題?聰明如你,可以在應用層根據uuid取模,來獲取相關的service。這樣就能保證同一資料的請求訊息,都會路由到同一個service。

5.主從DB與cache如何保證一致性

在只有主庫時,通過我們上面講的”序列化”的思路可以解決快取與資料庫不一致的問題。但是在”主從同步,讀寫分離的資料庫架構下”,有可能出現髒資料入快取的情況,此時序列化方案不再適用了,下面我們來討論一下這個問題。

5.1不一致的例子

http://static.oschina.net/uploads/space/2016/0409/104909_PoLz_818912.png

  1. 請求A發起了一個寫操作,第一步淘汰了cache(如上圖中的1)
  2. 請求A繼續寫資料庫,寫的是主庫,寫入最新資料(如上圖中的2)
  3. 請求B發起了一個讀操作,讀cache, 此時 cache中是空的(如上圖中的3)
  4. 請求B繼續讀資料庫,讀的是從庫,此時恰巧主從同步還沒有完成,讀出來一個髒資料,然後髒資料入cache(如上圖中的4)
  5. 最後資料庫的主從同步完成了(如上圖的5)

這種情況下,其實就是主從同步的時延期間,有讀請求讀從庫導致的不一致。這個問題怎麼優化呢?

5.2 解決思路

假設主從同步的時延<1s, 那麼舊資料就是在那1s的間隙中入快取的,是不是可以在寫請求完成後,再休眠1s, 再次淘汰快取,就能將這1s內寫入的髒資料再次淘汰掉呢?
Bingo, 當然是可以。

寫請求的步驟如下:

  1. 先淘汰快取
  2. 再寫資料庫
  3. 休眠1s, 再次淘汰快取

這樣的話保證一致性是沒有問題的,但是所有的寫請求都阻塞了1s, 大大降低了寫請求的吞吐量, 這是不可接受的。其實我們不需要休眠1s,而是直接將”淘汰快取的任務”交給一個非同步的timer來處理。
http://static.oschina.net/uploads/space/2016/0409/105244_VK0A_818912.png

多說一句,從架構的角度來看,其實我們可以將對快取,資料庫的操作獨立出來,提供一個統一的服務介面,這樣上層的service就不需要關注先操作快取,還是先運算元據庫等問題,我們的架構可以是這樣的:
http://static.oschina.net/uploads/space/2016/0409/105444_Egh5_818912.png
參考:
https://www.cnblogs.com/winner-0715/p/7451664.html

相關文章