緩存與數據庫的雙寫一致性
這幾天瞎逛,不知道在哪里瞟到了緩存的雙寫,就突然想起來這塊雖然簡單,但是細節上還是有足夠多我們可以去關注的點。這篇文章就來詳細聊聊雙寫一致性。
首先我們知道,現在將高速緩存應用於業務當中已經十分常見了,甚至可能跟數據庫的頻率不相上下。你的用戶量如果上去了,直接將一個裸的 MySQL 去扛住所有壓力明顯是不合理的。
這里的高速緩存,目前業界主流的就是 Redis 了,關於 Redis 相關的文章,之前也有聊過,在此就不贅述,感興趣的可以看看:
額,不列出來我都沒感覺關於 Redis 我居然寫了這么多...言歸正傳。
在我們的業務中,普遍都會需要將一部分常用的熱點數據(或者說不經常變但是又比較多的數據)放入 Redis 中緩存起來。下次業務來請求查詢時,就可以直接將 Redis 中的數據返回,以此來減少業務系統和數據庫的交互。
這樣有兩個好處,一個是能夠降低數據庫的壓力,另一個自不必說,對相同數據來說能夠有效的降低 API 的 RT(Response Time)。
后者其實還好,降低數據庫的壓力顯得尤為重要,因為我們的業務服務雖然能夠以較低的成本做到橫向擴展,但數據庫不能。
這里的不能,其實不是指數據庫不能擴展。MySQL 在主從架構下,通過擴展 Slave 節點的數量可以有效的橫向擴展讀請求。而 Master 節點由於不是無狀態的,所以擴展起來很麻煩。
對,是很麻煩,也不是不能橫向擴展。但是在那種架構下,我舉個例子,主-主架構下,會帶來很多意向不到的數據同步問題,並且對整個的架構引入了新的復雜性。
就像我在之前寫的MySQL 主從原理中提到過的一樣,雙主架構更多的意義在於 HA,而不是做負載均衡。
所以,相同的數據會同時存在 Redis 和 MySQL 中,如果該數據並不會改變,那就完美的一匹。可現實很骨感,這個數據99.9999%的概率是一定會變的。
為了維護 Redis 和 MySQL 中數據的一致性,雙寫的問題的就誕生了。
Cache Aside Pattern
其中最經典的方案就是 Cache Aside Pattern ,這套定義了一套緩存和數據庫的讀寫方案,以此來保證緩存和數據庫中的數據一致性。
具體方案
Cache Aside Pattern 具體又分為兩種 Case,分別是讀和寫。
對於讀請求,會先去 Redis 中查詢數據,如果命中了就會直接返回數據。而如果沒有從緩存中獲取到,就會去 DB 中查詢,將查詢到的數據寫回 Redis,然后返回響應。

而更新則相對簡單,但是也是最具有爭議。當收到寫請求時,會先更新 DB 中的數據,成功之后再將緩存中的數據刪除。

注意這里是刪除,而不是更新。因為實際生產中,緩存中存放的可能不僅僅是單一的像 true
、false
或者1
、19
這種值。
為什么是刪除
還有可能在緩存中存放一整個結構體,其中包含了非常多的字段。那么是不是每次有一個字段更新就都需要去把數據從緩存中讀取出來,解析成對應的結構體,然后更新對應字段的值,再寫回緩存呢?又或者你是直接將原緩存刪除,然后又將最新的數據寫入緩存?
其實乍一看,好像沒有毛病。我更新難道不應該這么更新嗎?在這里,我們的關注點更多的放在了更新的方式上,而把更多的必要性給忽略到了。我們更新了這個值之后,在接下來的一段時間內,它會被頻繁訪問到嗎?可能會,但也可能根本不會被訪問到了。
那既然有可能不會被訪問到, 那我們為啥還要去更新它?而且,更新緩存所帶來的開銷有時侯會非常大。
然而這還只是緩存數據源單一的情況,如果緩存中緩存的是某個讀模型,其數據是通過多張表的數據計算得出的,其開銷會更大。

讀模型,簡單理解就是用現有數據,計算、統計出來的一些數據。
這個思路就類似於懶加載的方式,只在需要的時候去計算它。
爭議在哪兒?
前面提到過,更新時順序為先更新 DB 中的數據,成功之后再刪除緩存。但是也有人認為應該先刪除緩存,再去更新 DB 中的數據。
乍一看,可能並不能發現問題。甚至覺得還有那么一絲絲合理。因為如果先刪除緩存,如果刪除操作失敗,DB 中的數據也不會更新,這樣緩存和 DB 中數據也能保證一致性。而且,如果刪除緩存成功,但更新 DB 失敗了,大不了下次獲取時,再將數據寫回緩存即可,可以說十分的合理。
但,這只是單線程的情況下,如果在多線程下,會直接造成致命的數據不一致。

上面的流程圖詳細的描述了情況,更新請求1剛剛把緩存中的數據刪除,查詢請求2就過來了,查詢請求2會發現緩存中是空的,所以按照 Cache Aside Pattern 的讀請求標准,會從 DB 中加載最新的數據並將其寫入緩存。而此時更新請求1還沒有對 DB 進行更新操作,所以查詢請求2寫入到緩存中的數據仍然是舊數據。
這樣一來,查詢請求3在下一次更新之前,讀取到的就都會是老數據。然后,更新請求1將最新的數據更新至 DB,緩存和 DB 的數據就不一致了。
其實 Cache Aside Pattern 中的模式,仍然會在某些 case 下造成數據不一致。但是這個概率非常的低,因為觸發這個不一致的情況的條件太苛刻了。

首先是緩存要失效,然后讀請求、寫請求並發的執行,並且讀請求要比寫請求后執行完。為啥說概率不大呢,首先在實際生產中,讀請求一般都要比寫請求快得多。除此之外,讀請求去 DB 請求數據的時間一定要早於寫請求,並且寫緩存的時間還要一定晚於寫請求,比起最開始的那種情況來說,條件已經是非常的嚴格了。
如果完全不能容忍,可以通過 2PC 的模式去保證數據的一致性,也可以通過將請求串行化的方式來解決,但這樣的代價就是會犧牲並發量。
End
其實還有其他的幾種方案,比如 Read Throught Pattern
、Write Through Pattern
、Write Around
、Write Behind Caching Pattern
等等。但是這些相對於 Cache Aside Pattern
來說比較簡單,可以自己去了解一下就好。