談談資料庫,快取一致性
幾年前,我在看部落格的時候,看到有一篇部落格的標題就是關於資料庫,快取一致性的,不以為然,直接跳過去了,心想,這麼簡單的問題還討論個鬼啊。這種想法持續了很久,直到某天,我看到越來越多的人都在討論資料庫,快取一致性的問題,才好好的看了下部落格,才發現原來資料庫,快取一致性真不是一個簡單的問題。今天我也來談談資料庫,快取一致性問題。
科普
考慮到有一些小夥伴可能技術不是那麼好,可能沒有接觸過快取,所以這裡還是花上一分鐘的時間,來介紹下什麼是快取,為什麼要有快取,以及資料庫和快取是如何搭配使用的。
讀取資料庫是比較耗時的操作,如果每次都需要去資料庫讀取資料,會對資料庫造成一定的壓力,程式效能也會比較低下,所以需要引入快取。
快取是提升程式效能的最重要、最有效、也是最簡單的手段之一。
引入快取後,讀操作會先去快取中看下,如果沒有命中快取,才去讀取資料庫,然後把讀取出來的資料再放到快取中去,這樣下一次讀操作就可以命中快取了,如果命中快取,就可以直接把資料返回出去了。
寫操作,除了修改資料庫,還需要刪除快取,因為不刪除快取,讀的操作讀到的永遠都是快取中的舊資料。
先刪除快取,後修改資料庫
這個方案顯然是有問題的。
兩個併發的讀寫操作:
- 一個寫的操作先進來,把快取刪除了;
- 在寫操作還沒有更新資料庫的時候,一個讀的請求又進來了,發現沒有命中快取,就去資料庫把老資料取出來了;
- 寫操作更新了資料庫;
- 讀操作把老資料放在了快取中。
這樣,資料庫中的資料和快取中的資料就不一致了,為了更好的讓大家理解這個過程,獻上一張醜到無法自拔的圖:
這個方案顯然不行,但是這個方案真的一無是處嗎?
非也,讓我們設想下這樣的場景:一個寫的請求進來,刪除快取,這個時候,Redis伺服器突然出問題了,或者網路突然出問題了,導致刪除快取失敗,丟擲了一個異常,導致程式沒有繼續執行修改資料庫的操作。從資料庫、快取一致性的角度來說,這裡很好的保證了資料庫、快取的一致性,兩者儲存的資料是一樣的,儘管儲存的都是老資料。
先修改資料庫,後刪除快取
相信絕大多數小夥伴都是運用的這個方案, 先前我覺得資料庫,快取一致性沒有什麼好討論的,太簡單了,就是因為我覺得這個方案是如此完美,但是後面我才慢慢發現這個方案也有一定的問題。
看到第一種方案存在的問題,大家也一定想到了這個方案也有同樣的問題。
在沒有快取的情況下,兩個併發的讀寫操作:
- 讀操作先進來,發現沒有快取,去資料庫中讀資料,這個時候因為某種原因卡了,沒有及時把資料放入快取;
- 寫的操作進來了,修改了資料庫,刪除了快取;
- 讀操作恢復,把老資料寫進了快取。
這樣就造成了資料庫、快取不一致,不過,這個概率出現的非常低,因為這需要在沒有快取的情況下,有讀寫的併發操作,在一般情況下,寫資料庫的操作要比讀資料庫操作慢得多,在這種情況下,還要保證讀操作寫快取晚於寫操作刪除快取才會出現這個問題,所以這個問題應該可以忽略不計。
說了這麼多,並沒有看到先修改資料庫,後刪除快取的致命問題啊,別急,讓我們繼續設想這樣的場景:一個寫的操作進來,修改了資料庫,但是刪除快取的時候 ,由於Redis伺服器出現問題了,或者網路出現問題了,導致刪除快取失敗,這樣資料庫儲存的是新資料,但是快取裡面的資料還是老資料,妥妥的資料庫、快取不一致啊。
延遲雙刪
可以看到修改資料庫,後刪除快取有兩個問題,雖然兩個問題都是低概率的,但是永遠追求完美的程式設計師可不能允許有這樣的事情發生,所以第三種方案出現了:延遲雙刪。
延遲雙刪就是先刪除快取,後修改資料庫,最後延遲一定時間,再次刪除快取。
這麼做就可以在一定程度上緩解上述兩個問題,第一次刪除快取相當於檢測下快取服務是否可用,網路是否有問題,第二次延遲一定時間,再次刪除快取,是因為要保證讀的請求在寫的請求之前完成。
但是這麼做,還是有一定問題,比如第一次刪除快取是成功的,第二次刪除快取才失敗,又該怎麼辦?
記憶體佇列
上面三種方式,都有一定的問題:
- 修改資料庫、刪除快取這兩個操作耦合在了一起,沒有很好的做到單一職責;
- 如果寫操作比較頻繁,可能會對Redis造成一定的壓力;
- 如果刪除快取失敗,該怎麼辦?
為了解決上面三個問題,第四種方式出現了:記憶體佇列刪除快取:寫操作只是修改資料庫,然後把資料的Id放在記憶體佇列裡面,後臺會有一個執行緒消費記憶體佇列裡面的資料,刪除快取,如果快取刪除失敗,可以重試多次。
這樣,就把修改資料庫和刪除快取兩個操作解耦了,如果刪除快取失敗,也可以多次嘗試。由於後臺有一個執行緒去消費記憶體佇列去刪除快取,不是直接刪除快取,所以修改資料庫和刪除快取之間產生了一定的延遲,這延遲應該可以保證讀操作已經執行完畢了。
但是這麼做也有不好的地方:
- 程式複雜度成倍上升,需要維護執行緒、佇列以及消費者;
- 如果寫操作非常頻繁,佇列的資料比較多,可能消費會比較慢,修改資料庫後,間隔了一定的時間,快取才被刪除。
但是這也是沒有辦法的事情,哪有十全十美的解決方案。
第三方佇列
一般來說,系統分為前臺系統和後臺系統,前臺系統主要是讀操作,後臺系統才有寫操作。
比如商品中心,前臺是面向使用者的,當用戶開啟商品詳情頁,會去快取中拿資料,後臺是面向業務人員的,業務人員可以在後臺系統對商品資訊進行修改。
如果是具有一定規模的公司,前臺系統和後臺系統肯定不在同一個伺服器上,而且是由不同的部門去負責的,所以記憶體佇列是肯定用不了的,如果後臺系統修改資料庫後,直接刪除快取,一定會發生如下的故事。
後臺系統 小明:你們前臺系統的產品詳情快取的key是什麼格式的?發我下。
前臺系統 小花:Product:XXXXX。
後臺系統 小明:好的。
過了幾天,小花找到小明。
前臺系統 小花:不對啊。你們怎麼沒有把活動中的產品詳情快取給刪掉啊?
後臺系統 小明:納尼,我怎麼知道你們是兩個快取啊,把活動中的產品詳情快取的key的格式發我下。
前臺系統 小花:Activity:Product:XXXX。
後臺系統 小明:好的。
過了幾天,訂單系統的開發又找到小明。
訂單系統 小強:你們修改了產品詳情後,還要把訂單中的產品詳情快取給刪除。
後臺系統 小明:。。。
過了幾天,廣告系統的開發又找到小明。
廣告系統 小王:你們修改了產品詳情後,還要把廣告中的產品詳情快取給刪除。
後臺系統 小明 卒,享年25。
如果引用了第三方佇列,如RabbitMQ,Kafka,小明就不會“卒”了,後臺系統的小明修改了資料庫後,不需要關心快取的事情,只要把資料的Id丟到訊息佇列,前臺系統、廣告系統、訂單系統的開發消費訊息佇列中的資料刪除快取。
上面說的幾種方案,都是比較常見的,也比較簡單,當然不同的方案也可以搭配使用,但是沒有“銀彈”,沒有完美的解決方案,就看你們的研發團隊,你們的場景適合哪種解決方案了。
今天的話題到這裡就結束了。