緩存和數據庫雙寫一致性

主要內容
   緩存熱點數據
   為何要使用緩存
   哪類數據適合緩存
   緩存的利與弊
   緩存和數據庫雙寫一致性
   不使用更新緩存而是刪除緩存
   先刪除緩存,還是先操作數據庫?
   我一定要數據庫和緩存數據一致怎麼辦
   實戰:先刪除緩存,再更新數據庫
   實戰:先更新數據庫,再刪緩存
   實戰:刪除緩存重試機制
   實戰:刪除緩存重試機制
   實戰:讀取binlog異步刪除緩存

緩存熱點數據
   在秒殺實際的業務中,一定有很多需要做緩存的場景,比如售賣的商品,包括名稱,詳情等。訪問量很大的數據,可以算是“熱點”數據了,尤其是一些讀取量遠大於寫入量的數據,更應該被緩存,而不應該讓請求打到數據庫上。

哪類數據適合緩存
   緩存量大但又不常變化的數據,比如詳情,評論等。對於那些經常變化的數據,其實並不適合緩存,一方面會增加系統的複雜性(緩存的更新,緩存臟數據),另一方面也給系統帶來一定的不穩定性(緩存系統的維護) 。

但一些極端情況下,你需要將一些會變動的數據進行緩存,比如想要頁面顯示準實時的庫存數,或者其他一些特殊業務場景。這時候你需要保證緩存不能(一直)有臟數據,這就需要再深入討論一下。

緩存的利與弊
    我們到底該不該上緩存的,這其實也是個trade-off的問題。

上緩存的優點:
能夠縮短服務的響應時間,給用戶帶來更好的體驗。
能夠增大系統的吞吐量,依然能夠提升用戶體驗。
減輕數據庫的壓力,防止高峰期數據庫被壓垮,導致整個線上服務BOOM!

上了緩存,也會引入很多額外的問題:
緩存有多種選型,是內存緩存,memcached還是redis,你是否都熟悉,如果不熟悉,無疑增加了維護的難度(本來是個純潔的數據庫系統)。
緩存系統也要考慮分佈式,比如redis的分佈式緩存還會有很多坑,無疑增加了系統的複雜性。
在特殊場景下,如果對緩存的準確性有非常高的要求,就必須考慮「緩存和數據庫的一致性問題」。

緩存和數據庫雙寫一致性
說了這麼多緩存的必要性,那麼使用緩存是不是就是一個很簡單的事情了呢,我之前也一直是這麼覺得的,直到遇到了需要緩存與數據庫保持強一致的場景,才知道讓數據庫數據和緩存數據保持一致性是一門很高深的學問。

不使用更新緩存而是刪除緩存
大部分觀點認為,做緩存不應該是去更新緩存,而是應該刪除緩存,然後由下個請求去去緩存,發現不存在後再讀取數據庫,寫入緩存。

原因一:線程安全角度
同時有請求A和請求B進行更新操作,那麼會出現

(1)線程A更新了數據庫

(2)線程B更新了數據庫

(3)線程B更新了緩存

(4)線程A更新了緩存

這就出現請求A更新緩存應該比請求B更新緩存早才對,但是因為網絡等原因,B卻比A更早更新了緩存。這就導致了臟數據,因此不考慮。

原因二:業務場景角度
有如下兩點:

(1)如果你是一個寫數據庫場景比較多,而讀數據場景比較少的業務需求,採用這種方案就會導致,數據壓根還沒讀到,緩存就被頻繁的更新,浪費性能。

(2)如果你寫入數據庫的值,並不是直接寫入緩存的,而是要經過一系列複雜的計算再寫入緩存。那麼,每次寫入數據庫後,都再次計算寫入緩存的值,無疑是浪費性能的。顯然,刪除緩存更為適合。

其實如果業務非常簡單,只是去數據庫拿一個值,寫入緩存,那麼更新緩存也是可以的。但是,淘汰緩存操作簡單,並且帶來的副作用只是增加了一次cache miss,建議作為通用的處理方式。

先刪除緩存,還是先操作數據庫?
那麼問題就來了,我們是先刪除緩存,然後再更新數據庫,還是先更新數據庫,再刪緩存呢?

對於一個不能保證事務性的操作,一定涉及“哪個任務先做,哪個任務後做”的問題,解決這個問題的方向是:如果出現不一致,誰先做對業務的影響較小,就誰先執行。

假設先淘汰緩存,再寫數據庫:第一步淘汰緩存成功,第二步寫數據庫失敗,則只會引發一次Cache miss。

假設先寫數據庫,再淘汰緩存:第一步寫數據庫操作成功,第二步淘汰緩存失敗,則會出現DB中是新數據,Cache中是舊數據,數據不一致。

「先刪緩存,再更新數據庫」
該方案會導致請求數據不一致

同時有一個請求A進行更新操作,另一個請求B進行查詢操作。那麼會出現如下情形:

(1)請求A進行寫操作,刪除緩存

(2)請求B查詢發現緩存不存在

(3)請求B去數據庫查詢得到舊值

(4)請求B將舊值寫入緩存

(5)請求A將新值寫入數據庫

上述情況就會導致不一致的情形出現。而且,如果不採用給緩存設置過期時間策略,該數據永遠都是臟數據。

所以先刪緩存,再更新數據庫並不是一勞永逸的解決方案,再看看先更新數據庫,再刪緩存
「先更新數據庫,再刪緩存」這種情況不存在並發問題麼?

不是的。假設這會有兩個請求,一個請求A做查詢操作,一個請求B做更新操作,那麼會有如下情形產生

(1)緩存剛好失效

(2)請求A查詢數據庫,得一個舊值

(3)請求B將新值寫入數據庫

(4)請求B刪除緩存

(5)請求A將查到的舊值寫入緩存

ok,如果發生上述情況,確實是會發生臟數據。

然而,發生這種情況的概率又有多少呢?

發生上述情況有一個先天性條件,就是步驟(3)的寫數據庫操作比步驟(2)的讀數據庫操作耗時更短,才有可能使得步驟(4)先於步驟(5)。可是,大家想想,「數據庫的讀操作的速度遠快於寫操作的(不然做讀寫分離幹嘛,做讀寫分離的意義就是因為讀操作比較快,耗資源少),因此步驟(3 )耗時比步驟(2)更短,這一情形很難出現。」
「先更新數據庫,再刪緩存」依然會有問題,不過,問題出現的可能性會因為上面說的原因,變得比較低!

所以,如果你想實現基礎的緩存數據庫雙寫一致的邏輯,那麼在大多數情況下,在不想做過多設計,增加太大工作量的情況下,請「先更新數據庫,再刪緩存!」

一定要數據庫和緩存數據一致怎麼辦
沒有辦法做到絕對的一致性,這是由CAP理論決定的,緩存系統適用的場景就是非強一致性的場景,所以它屬於CAP中的AP。
所以,我們得委曲求全,可以去做到BASE理論中說的「最終一致性」。
最終一致性強調的是系統中所有的數據副本,在經過一段時間的同步後,最終能夠達到一個一致的狀態。因此,最終一致性的本質是需要係統保證最終數據能夠達到一致,而不需要實時保證系統數據的強一致性

到達最終一致性的解決思路,主要是針對上面兩種雙寫策略(先刪緩存,再更新數據庫/先更新數據庫,再刪緩存)導致的髒數據問題,進行相應的處理,來保證最終一致性。
[延時雙刪]
問:先刪除緩存,再更新數據庫中避免臟數據?

答案:採用延時雙刪策略。

上文我們提到,在先刪除緩存,再更新數據庫的情況下,如果不採用給緩存設置過期時間策略,該數據永遠都是臟數據。

「那麼延時雙刪怎麼解決這個問題呢?」
(1)先淘汰緩存

(2)再寫數據庫(這兩步和原來一樣)

(3)休眠1秒,再次淘汰緩存

這麼做,可以將1秒內所造成的緩存臟數據,再次刪除。

「那麼,這個1秒怎麼確定的,具體該休眠多久呢?」
針對上面的情形,讀者應該自行評估自己的項目的讀數據業務邏輯的耗時。然後寫數據的休眠時間則在讀數據業務邏輯的耗時基礎上,加幾百ms即可。這麼做的目的,就是確保讀請求結束,寫請求可以刪除讀請求造成的緩存臟數據。

「如果你用了mysql的讀寫分離架構怎麼辦?」
在這種情況下,造成數據不一致的原因如下,還是兩個請求,一個請求A進行更新操作,另一個請求B進行查詢操作。

(1)請求A進行寫操作,刪除緩存

(2)請求A將數據寫入數據庫了,

(3)請求B查詢緩存發現,緩存沒有值

(4)請求B去從庫查詢,這時,還沒有完成主從同步,因此查詢到的是舊值

(5)請求B將舊值寫入緩存

(6)數據庫完成主從同步,從庫變為新值

上述情形,就是數據不一致的原因。還是使用雙刪延時策略。只是,睡眠時間修改為在主從同步的延時時間基礎上,加幾百ms。

「採用這種同步淘汰策略,吞吐量降低怎麼辦?」
那就將第二次刪除作為異步的。自己起一個線程,異步刪除。這樣,寫的請求就不用沉睡一段時間後了,再返回。這麼做,加大吞吐量。

「所以在先刪除緩存,再更新數據庫的情況下」,可以使用延時雙刪的策略,來保證臟數據只會存活一段時間,就會被準確的數據覆蓋。

「在先更新數據庫,再刪緩存的情況下」,緩存出現臟數據的情況雖然可能性極小,但也會出現。我們依然可以用延時雙刪策略,在請求A對緩存寫入了臟的舊值之後,再次刪除緩存。來保證去掉臟緩存。

刪緩存失敗了怎麼辦:重試機制
看似問題都已經解決了,但其實,還有一個問題沒有考慮到,那就是刪除緩存的操作,失敗了怎麼辦?比如延時雙刪的時候,第二次緩存刪除失敗了,那不還是沒有清除臟數據嗎?

「解決方案就是再加上一個重試機制,保證刪除緩存成功。」
方案一:
流程如下所示

(1)更新數據庫數據;

(2)緩存因為種種問題刪除失敗

(3)將需要刪除的key發送至消息隊列

(4)自己消費消息,獲得需要刪除的key

(5)繼續重試刪除操作,直到成功

然而,該方案有一個缺點,對業務線代碼造成大量的侵入。於是有了方案二,在方案二中,啟動一個訂閱程序去訂閱數據庫的binlog,獲得需要操作的數據。在應用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。

方案二:
流程如下圖所示:

(1)更新數據庫數據

(2)數據庫會將操作信息寫入binlog日誌當中

(3)訂閱程序提取出所需要的數據以及key

(4)另起一段非業務代碼,獲得該信息

(5)嘗試刪除緩存操作,發現刪除失敗

(6)將這些信息發送至消息隊列

(7)重新從消息隊列中獲得該數據,重試操作。

「可以總結為如下幾點:」

對於讀多寫少的數據,請使用緩存。
為了保持一致性,會導致系統吞吐量的下降。
為了保持一致性,會導致業務代碼邏輯複雜。
緩存做不到絕對一致性,但可以做到最終一致性。
對於需要保證緩存數據庫數據一致的情況,請盡量考慮對一致性到底有多高要求,選定合適的方案,避免過度設計。

留言

  1. Casinos Near Casinos in Waco, WV | MapYRO
    Find Casinos Near Casinos in Waco, WV. See map. Realtime 정읍 출장안마 driving 속초 출장마사지 directions 동두천 출장안마 to 양주 출장마사지 casinos in Waco, WV. 동두천 출장샵

    回覆刪除

張貼留言

這個網誌中的熱門文章

Json概述以及python對json的相關操作

Docker容器日誌查看與清理

利用 Keepalived 提供 VIP