02.28 如何保證本地緩存、分佈式緩存、數據庫之間的數據一致性?

yuanyuanaiyl


在現在的系統架構中,緩存的地位可以說是非常高的。因為在互聯網的時代,請求的併發量可能會非常高,但是關係型數據庫對於高併發的處理能力並不是非常強,而緩存由於是在內存中處理,並不需要磁盤的IO,所以非常適合於高併發的處理,也就成為了各個系統中必不可少的一部分了。

不過,由此產生的問題也是非常多的,其中一個就是如何保證數據庫和緩存之間的數據一致性。

由於數據庫的操作和緩存的操作不可能在一個事務中,也就勢必會出現數據庫寫入失敗,緩存不能更新,緩存寫入失敗的補償機制。具體我們應該怎麼做呢?

我們先看一個最常見的讀緩存的例子

在讀取緩存的方式中,上圖這種方式可以說是最為廣泛使用的了。讀本身是沒有什麼問題的,但是,寫入緩存的方式,就是保證數據一致性的重中之重了。

這裡我們不考慮定時刷新緩存的方式,也就是下面這類方式:

寫入數據庫和寫入緩存是獨立的,寫入數據庫操作後,需要等待定時服務執行,執行完成後緩存數據才會刷新。

這種方式會導致數據的不一致時間較長,數據刷新時,不管有沒有改變的數據,都會重新加載,效率差。當然,並不是說這種方式就沒用,還是有一些場景是可以使用的,例如一些系統配置的緩存,而且,這樣做緩存刷新,代碼量非常少,也便於維護。

我們今天只考慮雙寫的數據一致性如何來考慮。由於不同的寫入方式,可能帶來的結果也就是不同的。通常情況下,我們都有哪些寫入數據並刷新緩存的方式呢?

方法一、先更新數據庫,在更新緩存

這套方案是最簡單的一種緩存雙寫方案,我們先來看看流程圖

使用這種雙寫的方案,只要在數據成功寫入數據庫後,刷新緩存就可以了,代碼簡單,維護也很簡單。但是,簡單的前提下,帶來的問題也是很直接的。

首先,線程數據安全無法保證

例如:我們現在同時有兩個請求會操作同一條數據,一個是請求A,一個是請求B。請求A需要先執行,請求B後執行,那麼數據庫的記錄就是請求B執行後的記錄。

但是,由於一些網絡原因或者其他情況,最終執行的順序可能就變成了:

請求A Update 數據庫 -> 請求B Update 數據庫 -> 請求B Update 緩存 -> 請求A Update 緩存。

這樣的結果會導致:

1. 數據庫和緩存中的數據不一致,從而緩存中的數據就成為了髒數據。

2. 寫入操作多於讀操作,就會頻繁的刷新緩存,但是這些數據根本沒有被讀過。這樣就會浪費服務器的資源。

因此,這種雙寫方式很難保證數據一致性,不建議使用。

方法二、先刪除緩存再更新數據庫

由於上述方式存在的問題,那麼我們就考慮,能不能先刪除緩存,在更新數據庫,這樣,在更新數據庫的前後,由於緩存中沒有數據了,請求就會穿透到數據庫直接讀取數據然後放入緩存,這樣,緩存就不會被頻繁的刷新了。

於是,我們就設置了一個新的執行順序:

不過,這樣一來,新問題又出現了。有兩個請求,一個請求A,一個請求B,請求A去寫數據,請求B去讀數據。當併發量高的時候,就會出現以下情況:

請求A進行寫操作,刪除緩存 -> 請求B查詢發現緩存不存在 -> 請求B去數據庫查詢得到舊值 -> 請求B將舊值寫入緩存 -> 請求A將新值寫入數據庫

這是,髒數據又出現了。如果我們沒有設置緩存的過期時間,那麼在下一次下入數據前,髒數據就會一直的存在。針對這種髒數據出現的情況,我們決定在寫入數據後,增加一點延時,再刪除一次數據,於是就有了方法三。

方法三、延時雙刪

使用延時雙刪的策略,就能夠很好的解決之前我們應該併發所引起的數據不一致的情況。那是不是延時雙刪就完全沒有問題呢?不。

我們來假設一個場景,就是我們做了讀寫分離,那麼使用延時雙刪可能問出現什麼情況呢?

請求A進行寫操作,刪除緩存 -> 請求A將數據寫入數據庫了 -> 請求B查詢緩存發現,緩存沒有值 -> 請求B去從庫查詢,這時,還沒有完成主從同步,因此查詢到的是舊值 -> 請求B將舊值寫入緩存 -> 數據庫完成主從同步,從庫變為新值。

糟糕,又出現數據不一致了。

然後在看看性能如何,由於需要延時,如果是同步執行,性能必定很差,所以第二次刪除只有做成異步,避免影響性能。那異步執行刪除就會出現新問題,如果異步線程執行失敗了,那麼舊數據就不會被刪除,數據不一致又出現了。

不行,我們需要向一個一勞永逸的辦法,單純的雙刪還是不可靠。

方法四、隊列刪除緩存

我們在把數據更新到數據庫後,把刪除緩存的消息加入到隊列中,如果隊列執行失敗,就再次加入到隊列執行直到成功為止。

這樣,我們就能夠有效的保證數據庫和緩存的數據一致性了,不管是讀寫分離還是其他情況,只要隊列消息能夠保證安全,那麼緩存就一定會被刷新。

當然,根據這個方案,我們還可以進一步優化。因為這裡我們的緩存刷新時基於業務代碼的,也就是說,業務代碼和緩存刷新的耦合度很高。有沒有辦法能夠把緩存刷新獨立出來,不基於業務代碼執行呢?

方法五、binlog訂閱刪除緩存

為了保證業務代碼的獨立性,我們可以通過訂閱binlog日誌的方式來刷新緩存。我們先啟動mysql的binlog日誌,然後如下圖方式設計流程:

通過binlog的訂閱,我們就把業務代碼和緩存刷新的非業務代碼獨立開來。代碼量小了,也方便維護了。程序員們也不需要去關心什麼時候應該刷新緩存,是不是需要刷新緩存。

當然,實戰中,我們還有很多不同的業務場景,可能需要的數據一致性同步方案也不同,這裡也只算是一個案例。


分享到:


相關文章: