「系統(tǒng)架構(gòu)」緩存與數(shù)據(jù)庫的數(shù)據(jù)一致性方案介紹
在很多系統(tǒng)中重要數(shù)據(jù)通常都是寫入關(guān)系數(shù)據(jù)庫如mysql中,為了實現(xiàn)讀寫分離,提高系統(tǒng)負載能力,縮短響應(yīng)時間通常還需要用到緩存。
緩存帶來了系統(tǒng)性能的提升同時也把數(shù)據(jù)一致性問題擺在了開發(fā)者面前,在數(shù)據(jù)庫使用讀寫分離和主從同步的情況下這種一致性問題會變得更加復雜。本文將介紹幾種提升一致性的方案供大家參考。
背景介紹
一般使用緩存(本文中的緩存不特指某一種分布式緩存或本地緩存)的方式為在讀數(shù)據(jù)時首先讀取緩存,如果緩存沒有則讀數(shù)據(jù)庫然后將數(shù)據(jù)寫入緩存最后返回;寫數(shù)據(jù)時首先清除緩存內(nèi)的數(shù)據(jù),然后寫數(shù)據(jù)庫。
這種方式在數(shù)據(jù)庫配置了主從庫時會遇到數(shù)據(jù)不一致的問題,首先來看一下這種實現(xiàn)的具體流程如下圖:
圖1 主從數(shù)據(jù)不一致
在讀數(shù)據(jù)時如果緩存中沒有數(shù)據(jù)則讀取從庫的數(shù)據(jù),然后寫入緩存中并返回;在寫數(shù)據(jù)時先清除緩存中的數(shù)據(jù),然后將數(shù)據(jù)寫入主庫,主庫數(shù)據(jù)會被同步到從庫。
這種實現(xiàn)方式的主要問題在于當數(shù)據(jù)寫入主庫后,緩存沒有數(shù)據(jù),這時讀請求會讀取從庫的數(shù)據(jù)。此時如果發(fā)生主從延遲,主庫的數(shù)據(jù)還沒有寫到從庫,則應(yīng)用服務(wù)器會將從庫讀到的臟數(shù)據(jù)寫入緩存服務(wù)器中,如果寫入的數(shù)據(jù)沒有加有效期或有效期很長就會造成數(shù)據(jù)不一致,如果主從延遲時間較長可能會導致大面積的數(shù)據(jù)不一致。下面將介紹幾種解決數(shù)據(jù)一致性問題的方案。
加有效期
給緩存中的數(shù)據(jù)增加有效期是解決一致性問題最簡單的確保數(shù)據(jù)最終一致性的方法,這種方法在緩存中沒有數(shù)據(jù)需要查詢數(shù)據(jù)庫時將查詢結(jié)果放入緩存的時候設(shè)置一個有效期(更新數(shù)據(jù)時仍然先清除緩存數(shù)據(jù)),非常適用于更新頻率較低的數(shù)據(jù),例如商品信息。
但是單純給數(shù)據(jù)加上有效期也存在一些明顯的問題,如果有效期較長就會出現(xiàn)上面提到的數(shù)據(jù)不一致的問題,如果有效期較短就會出現(xiàn)緩存效率不高經(jīng)常讀庫的情況。在使用這種方法的時候就需要我們根據(jù)數(shù)據(jù)的更新頻率確定合適的有效期時間,當冷熱數(shù)據(jù)并存時這種方案就顯得難以兼顧。那么什么策略既能確保數(shù)據(jù)的最終一致性又能充分利用緩存呢?這就要提到業(yè)內(nèi)使用最多的雙淘汰了。
雙淘汰
雙淘汰與本文第一個方案相比在讀取數(shù)據(jù)時是相同的,區(qū)別在于更新數(shù)據(jù)的流程。在更新數(shù)據(jù)時仍然首先清除緩存的數(shù)據(jù),然后將數(shù)據(jù)寫入到數(shù)據(jù)庫中,然后將數(shù)據(jù)記錄在一個延遲隊列或哈希表中,同時另一個線程不斷讀取延遲隊列或者哈希表,根據(jù)數(shù)據(jù)存入的時間也預(yù)先設(shè)定的延遲時間再次清除緩存了的數(shù)據(jù)。
可以看出預(yù)先設(shè)定的延遲時間應(yīng)該大于數(shù)據(jù)庫主從同步較慢情況下的同步時間,這樣就能確保在主從延遲的情況下緩存中的臟數(shù)據(jù)也能被清除保證了數(shù)據(jù)一致性。流程如下圖,C語言中可以使用哈希表實現(xiàn)。
雖然雙淘汰保證了數(shù)據(jù)的最終一致性并提高了緩存的使用率,但在兩次“淘汰”之間讀取的數(shù)據(jù)仍然有可能是臟數(shù)據(jù),這種情況會在主從延遲較長的情況下尤為明顯。對于某些對實時一致性要求較高的系統(tǒng)如何獲得更好的讀一致性呢,這里需要提到雙淘汰的另一種變型。

圖2 雙淘汰
另一種雙淘汰
為了提高讀到數(shù)據(jù)的準確性這種方法在更新數(shù)據(jù)時首先清除緩存數(shù)據(jù)并在緩存中存入這個數(shù)據(jù)對應(yīng)的標記,在寫入數(shù)據(jù)庫成功后再將數(shù)據(jù)寫入到一個延遲隊列或哈希表中。在讀取數(shù)據(jù)時從緩存讀取數(shù)據(jù),如果存在直接返回,如果不存在則讀取數(shù)據(jù)對應(yīng)的標記,如果標記存在則讀主庫否則讀從庫,最后將數(shù)據(jù)寫入緩存中。
同時另一個線程不斷讀取延遲隊列或者哈希表,根據(jù)數(shù)據(jù)存入的時間也預(yù)先設(shè)定的延遲時間再次清除緩存了的數(shù)據(jù)。整個讀寫過程如下圖??梢钥闯鲞@種方法通過在緩存增加一個標記將部分讀請求分流到了主庫,這個標記可以是數(shù)據(jù)的主鍵或其他唯一標識,通過犧牲一部分主庫的性能提高了讀請求的數(shù)據(jù)一致性。

圖3 雙淘汰2
目前為止沒有哪一種緩存策略是萬能的,基本上我們?nèi)孕枰鶕?jù)具體的業(yè)務(wù)場景和數(shù)據(jù)類型選擇合適的緩存策略。數(shù)據(jù)量越大數(shù)據(jù)情況也復雜通常就需要越復雜的緩存策略,希望本文介紹的幾個方案對讀者今后的開發(fā)有所幫助。

































