上一篇我們在 緩存雪崩,穿透,擊穿中講到了爲什麼使用緩存,以及使用緩存可能會遇到哪些問題?其中就有緩存和數據庫雙寫一致性問題。那什麼是緩存和數據庫雙寫一致性問題呢?是什麼導致了一致性的問題呢?
什麼是緩存和數據庫雙寫一致性問題?
數據庫中的數據和緩存中的數據不一致
如果我們只是讀操作,肯定不會存在緩存和數據庫雙寫一致性問題。但是如果更新或者刪除操作呢?
是什麼導致了數據庫和緩存雙寫一致性問題?
我們知道執行一個更新操作花費的時間遠遠大於一個讀操作花費的時間。更新操作,我們是先更新數據庫呢還是先更新緩存呢?
如果兩步操作符合原子性,要麼同時成功,要麼同時失敗,則不存在一致性的問題,如果原子操作被破壞了,則會發生數據庫和緩存雙寫一致性問題。
這兩步操作並沒有誰先誰後是沒關係的,破壞原子性可能會存在以下四種情況。
- 操作數據庫成功,操作緩存失敗。
- 操作緩存成功,操作數據庫失敗。
- 操作數據庫失敗,操作緩存成功。
- 操作緩存失敗,操作數據庫成功。
無論是先操作數據庫還是先操作緩存,如果第一步失敗了(第3,4 種情況),直接報異常,第二步也就不執行了,所以我們完全可以忽略第3,4 種情況。
補充
:對於操作緩存,有更新和刪除兩種,少使用更新操作,有時候更新一個key所對應的value,這個vaule 值是經過多個表聯查計算得出的結果,這個操作是非常慢的,頻繁更新,更影響性能,其次該key單位時間內被讀取的次數比較少。與其每次更新緩存,倒不如在需要讀的時候,直接刪除緩存,訪問的緩存沒有,此時就會訪問數據庫,然後重新計算一次即可。所以後面講述的時候,更新操作統稱刪除操作。(用的時候再去查最新的值,這是一種懶加載的計算思想)
情況一: 先更新數據庫成功,刪除緩存失敗。(Cache Aside Pattern設計模式)
更新數據庫成功,更新緩存失敗,此時緩存中的數據爲舊數據。
併發場景下可能會出現一下問題:
- 緩存失效
- 線程A查詢數據庫,得一箇舊值
- 線程B將新值寫入數據庫(寫數據庫操作花費時間比讀操作長,寫操作需要鎖表)
- 線程B刪除緩存
- 線程A將查到的舊值寫入緩存
上述雙寫一致性問題發生的概率較小,既要剛好緩存失效,還併發這寫操作。且讀操作在寫操作之前,又必須比更新緩存操作晚。
Cache Aside Pattern
如何避免刪除緩存失敗呢?
- 重試刪除操作,直到成功
- 將刪除的 key 發送到消息隊列
- 採用消息隊列,消費消息,獲取要刪除的key
情況二: 先刪除緩存成功,更新數據庫失敗。
併發場景下可能存在以下情況:
- 線程A刪除了緩存
- 線程B查詢,緩存不存在,去數據庫查詢,得到舊值
- 線程B將舊值寫入緩存
- 線程A將新值寫入數據庫
解決
: 採用消息隊列,將 刪除緩存,修改數據庫,讀緩存三步操作放入隊列,實現串行化。
比較
先更新數據庫,在刪緩存,高併發請求下,可以緩解數據庫壓力,性能好,但是拿到的數據時舊數據。
先刪緩存,後更新數據庫,高併發請求下,訪問數據庫,數據庫壓力大,性能差,但是拿到的數據時新數據。