最近幫組裡做講座預約系統,雖然使用人數不多,但終於還是遇到了一些系統經典問題,比如資料庫與快取的一致性問題,很有意思,好記性不如爛筆頭,學習了一些思路以後決定記錄下來與大家分享。
程式設計師應該沒人不懂這個,但我還是覺得應該寫上,有頭有尾。所謂資料庫與快取的一致性問題,可以說是伴隨著計算機這個東西一路走來的古老問題。
就我所知最早的快取一致性問題經典案例就是CPU的Cache與記憶體之間的快取一致性問題。
學過計組的都知道,每個CPU都有自己獨享的快取記憶體Cache,而Cache本質上是對於記憶體的快取,當多個CPU共用一個記憶體時,該問題就來了,比如A,B兩個CPU的快取記憶體中都儲存了記憶體上0x00001111
這個地址的資料,如果此時A處理完了該資料並寫回了記憶體,那麼顯然B的Cache中的該資料就過期了,如果B又讀取了該資料進行處理,那麼就使用了錯誤的資料。我們必須保證所有CPU讀取到的快取中的內容是真實的,不然處理虛假的資料只會造成錯誤的結果。而這一點引申至一切資料庫+快取的結構中都適用。
CPU的解決方案是基於匯流排嗅探機制的 MESI 協定,這個大家也都學過不細說,總之該協定保證了寫傳播和事物的序列化,解決了CPU的快取一致性問題,保證了我們使用計算機所獲得的服務質量。
扯遠了,總之快取一致性問題本質上就是快取資料與資料庫資料之間的同步問題,一旦資料庫中的資料被修改,就必須要讓所有快取了該資料的使用者都知道該資料快取已經失效,需要讀取最新值。
解決方案無論好壞我都列在下面,如果你希望找一個靠譜的方案請選後面的,前面的例子主要還是給自己看看理解理解。
先寫快取,再寫資料庫
目前沒人會用的方案,先寫快取風險太大,因為要明確當今主流的微服務架構下,任何服務都是不那麼可靠的,如果先寫快取成功,再寫資料庫卻失敗了,這時我們的快取中就出現了假資料,這是不可接受的,所以目前這種方案採用的很少。
先寫資料庫,再寫快取
雖然沒有假資料那麼嚴重但還是存在同樣的問題,如果先寫資料庫成功,再寫快取失敗,那麼資料庫中資料雖然真實但是也讀取不到,還是沒有意義,指望服務自己爭氣不要出錯等同於給自己埋雷。
也有人在這裡會說可以把寫資料庫和寫快取都放在一個事務中,藉助事務的原子性來保證正確。這還是會存在非常多的問題,在小並行量下勉強能用,但是這個做法將會嚴重影響介面效能,不過有時候我很懷疑學校自己的搶課系統是不是就是這麼做的,不然怎麼能每次搶課都那麼卡...
但是一旦並行量起來,這個方案還是會遇到先後順序的問題,比如A,B兩個使用者在幾乎相同的時間開啟了事務準備寫回資料,其中A先寫完了資料庫,但是寫快取時網路波動被延遲,所以又慢於B寫快取,那麼兩個事務執行完,你就會發現資料庫中是B寫的,快取中是A寫的,還是不一致,更不用說每次寫資料庫操作還需要附帶一次寫快取,本身就是對於系統資源的一種浪費。
那如果通過加鎖來防止並行事務出錯,首先你需要在這裡引入分散式鎖問題,相當複雜,其次,這將進一步影響本來就不太行的系統效能,大大折損整個系統的吞吐量,所以總的來說這個方案還是拋棄比較好。
先刪除快取,再寫資料庫
高並行下容易出現問題的方案,老樣子A,B兩個使用者,同時發起請求,A打算寫,B打算讀。
假設A刪除了快取,然後網路卡頓,沒及時更新資料庫。這時B請求,快取未命中,於是請求資料庫,查到了舊值,隨後寫入了快取。此時A的卡頓結束,更新了資料庫。你就會發現資料庫中是新值,快取是舊值,二者不一致。
那麼能不能對這個問題再進行解決呢?還真的有,那就是快取雙刪,很好理解,就是寫之前刪除一次,寫完以後再刪一次,這樣就能保證後面的快取和資料庫的一致性了。不過這裡要注意一點,那就是第二次刪除一定要間隔一段時間,不能一完成資料庫的更新就立馬刪除,因為此時資料庫剛剛更新,可能有別的請求正拿著舊資料還沒寫完快取,你前腳剛刪它後腳就又寫上了,那不是白費力氣嗎?所以這裡必須要隔一段緩衝時間,等讀了舊資料的請求都處理完了,再去第二次刪除快取。
不過這裡還有一個問題,如果雙刪的第二次刪除失敗了怎麼辦呢?這裡先按下不表,後面再聊。
先寫資料庫,再刪除快取
這個看起來非常合理,上面那個方案既然先的那一次刪快取會導致一致性失效,那麼我乾脆不做第一次刪除,更新資料庫後,隔一段時間我再刪除不就可以保證快取一致性了嗎?
沒錯,這種情況下想要出差錯非常困難,只有當滿足以下三個條件時才會發生錯誤
問題:刪除快取的方案如果刪除失敗了怎麼辦?
答:加入重試機制,若更新資料庫成功,但更新快取失敗,我們就需要重試此操作,如果重試成功,那麼一切照舊沒有問題,如果重試到了指定最大次數還沒有成功,那麼我們寫入資料庫並等待後續處理。
但是這裡的重試機制水同樣很深,如果同步重試,並行量高時非常影響效能。而非同步重試就引入了非常多的可能和變數。所以這裡也產生了很多種用於處理刪除快取的方案。
如下