MySQL Redis如何實現緩存一致

2024年2月6日 21点热度 0人点赞

在日常的工作和學習中,我們可能經常會看到關於 MySQL 和 Redis 如何才能保證緩存一致的問題,本篇文章就來帶你了解一下,這到底是個什麼東西。話不多說,直接開整~~

為了防止有些盆友不太了解什麼是緩存一致及為什麼要這麼做,我先來小小的解釋一下: MySQL/Redis緩存一致性是指在使用MySQL作為持久化數據庫和Redis作為緩存系統的應用場景中,確保當數據在MySQL中發生更改時,這些更改能夠被及時、正確地反映到Redis的緩存中。保持緩存一致性是為了避免出現以下情況:

  • 臟讀:從緩存中讀取到了已經過期或者已經被刪除的數據。
  • 不可重復讀:在短時間內連續兩次讀取相同的數據,但得到的結果不一致。
  • 幻讀:因為並發操作導致讀取的數據與實際存儲的數據不同。

好了先來個圖,大概解釋一下有哪些方法(這裡給出的方法隻是基於M/R讀寫刪除相關的,其它中間件或者鎖等方法,等俺下次補充):

我們來一個個的看,先說這個不實用的方案,之所以說它不實用並不是否定這個方法的作用,畢竟存在即合理嘛,隻是說它們在一些特定的場景中才會發揮出作用。

(註:以下圖解都是以時序圖解釋的,相信大傢都知道吧~~)

不實用的方案

1. 先寫 MySQL , 再寫 Redis

相信大傢從圖中就可以看出為什麼說這是一個不推薦的方法了,兩個請求都是先寫MySQL,再寫Redis,再並發的場景下,就可能會發生MySQL中的數據更新了,但Redis中還是舊值的情況。值得註意的一點是,再這個場景下,對於讀請求一般的處理都是先讀Redis,沒有的話再去數據庫中查詢,但是這個讀請求並不會更新Redis中的數據。即,讀請求不會更新Redis。

2. 先寫 Redis , 再寫MySQL

和前一種的方法一樣,這裡就不多做解釋了,看圖想一想就會明白。同樣是在高並發的場景下會出現一些問題。

3. 先刪 Redis,再寫 MySQL

這裡需要提醒一下,請求A是一個更新操作,請求B是一個讀操作(在這個場景下,請求B是會將讀到的數據回寫到Redis中的)。 這個可能出現緩存不一致的處理方案,在日常開發中是經常會發生的。

因為更新操作可能耗時會比較久,所以在請求A 刪除了緩存後,請求B的讀請求可能會在這期間執行,而且查詢操作還是很快的。剩下的看圖就可以理解了。

實用的方案

1. 先刪 Redis,再寫 MySQL, 再刪 Redis

這就是我們經常可用聽到的“緩存雙刪”、“延遲雙刪除”等詞。

這種方法看起來好像真的可以解決緩存不一致的問題,但是對於這個延遲刪除緩存,我們到底該什麼時間刪呢?

以下有幾個可供參考的方法:

定時任務: 可以在應用程序中設置一個定時任務,在第一次刪除緩存後延遲指定的時間,然後執行第二次刪除緩存的操作。呃,這個說白了就是讓請求A 的最後一次刪除緩存操作等待一段時間再去執行。

異步處理: 在更新數據庫成功後,啟動一個異步任務(例如使用 RabbitMQ、Kafka 等消息隊列或者線程池),該任務在延遲指定的時間後執行第二次刪除緩存的操作。

Redis過期機制:設置Redis鍵的過期時間(TTL)為一個較短的值,這樣即使在第二次刪除之前有新請求將舊數據寫入緩存,也會在短時間內自動過期並被刪除。

對於第一個方法,可能是一個“好用”,但不實用的方法,這種隻需要一個sleep xxx ms,就能完成的功能,可不就是好用嗎(doge)。說它不實用是因為它不可控,具體的就不說了。

然而,盡管存在這些不可控因素,定時任務仍然是實現延遲雙刪等策略的一種常見方法。畢竟哪有那麼多的高並發的場景啊,還有就是我們程序員“懶”,功能實現了就行了​。

異步處理這個方法感覺是以上幾個方法中,較好的,實現了功能的同時還保證了穩定。但是麻煩。。。因為需要添加一個新的組件。

下面是這個方法需要註意的幾個點:

定義異步任務:首先,你需要定義一個異步任務,這個任務負責在數據庫更新後執行第二次刪除Redis緩存的操作。這個任務可以是一個實現了特定接口或者繼承了特定類的類,具體取決於你使用的編程語言和框架。

更新數據庫並觸發異步任務:當需要更新數據庫時,首先執行第一次刪除Redis緩存的操作,然後進行數據庫的更新。在數據庫更新成功後,立即觸發定義好的異步任務。這通常可以通過以下方式實現:

使用消息隊列(如RabbitMQ、Kafka等):將一個消息發送到隊列中,消息的內容包含了需要刪除的緩存鍵或者其他相關的信息。然後,有一個消費者服務監聽這個隊列,接收到消息後執行第二次刪除緩存的操作。

使用線程池或者異步執行框架:直接在應用程序中創建一個新的線程或者任務,將其提交到線程池或者異步執行框架中。這個新任務會在後臺運行,並在延遲指定的時間後執行第二次刪除緩存的操作。

實現異步任務邏輯:在異步任務的實現中,需要包含以下邏輯: 延遲執行:根據業務需求,設置一個合適的延遲時間。這個時間應該足夠長,以覆蓋從第一次刪除緩存到數據庫更新完成之間的時間窗口,但又不能太長,以免影響數據的一致性。 刪除緩存:在延遲時間過後,執行第二次刪除Redis緩存的操作。這通常涉及到與Redis服務器的通信,使用適當的Redis客戶端庫來發送刪除命令。

處理異常和重試:在異步任務的執行過程中,可能會遇到各種異常情況,如網絡中斷、Redis服務器故障等。為了保證數據的一致性和系統的穩定性,需要實現適當的異常處理和重試機制。例如,如果刪除緩存失敗,可以嘗試重新發送刪除命令,或者記錄錯誤日志並通知運維人員。

具體消息對列的使用,等之後再說吧。

對於第三種方法,我感覺還沒第一種好,他的一下問題如下:

數據一致性問題:延遲雙刪策略的主要目的是確保在更新數據庫後,能夠及時刪除相關的Redis緩存,以避免數據不一致。而Redis的過期機制是基於時間的,它不能精確地與數據庫更新操作同步,因此可能會導致在鍵過期和實際刪除之間的時間窗口內,新的請求查詢到舊的數據庫數據並將其放入緩存。

過期時間設置復雜性:為了使用過期機制替代延遲雙刪,需要為每個緩存鍵設置一個合適的過期時間,這個時間需要考慮到數據庫更新的頻率、網絡延遲、系統負載等因素,設置起來可能會比較復雜和難以準確預測。

內存管理問題:如果大量緩存鍵在同一時間過期,可能會引發Redis的內存管理和垃圾回收壓力,影響系統的性能和穩定性。

無法應對異常情況:如果在鍵過期和實際刪除之間發生系統故障或者網絡中斷等情況,可能會導致數據不一致或者緩存失效的問題。

扯遠了,回到正軌。。

2. 先寫 MySQL , 再刪 Redis

對於一些可以允許系統出現短時間不一致的場景還是可以使用的,但對於一些強一致的場景就不太適用了,比如秒殺、庫存扣減等業務。

對於這個方法,還有一種特殊的場景,就是請求發出時,緩存剛好失效,可能會引發的錯誤。

實際上這種遇到的概率是非常小的,一是緩存剛好失效,二是請求B從數據庫查到數據,且回寫緩存的耗時,要比請求A寫數據庫且刪除緩存的時間要長。這在實際開發中遇到的概率極低。

3. 先寫MySQL,通過BinLog,異步更新 Redis

在這個方法中,這個查詢的請求是不會回寫數據到Redis中的。

對於這個方法,會保證MySQL和Redis的最終一致性,但是如果中途請求B需要查詢數據,如果緩存無數據,就直接查DB;如果緩存有數據,查澗的數據也會存在不一致的情況。

所以這個方案,是實現最終一致性的終極解決方案,但是不能保證實時性

幾種方案的對比

1. 先寫 Redis , 後寫MySQL

數據沒有落到數據庫中你們安心嗎,hxd,萬一數據庫宕機了,我們隻靠緩存那不是開玩笑嗎,正式環境中使用這種方法,就要提桶跑路了​編輯

2. 先寫 MySQL, 後寫 Redis

對於並發量不大、不要求強一致的使用還是挺不錯的,但缺點是怕Redis突然掛了。。

3. 先刪 Redis, 再寫 MySQL

呃,這個的話好像沒什麼可以用的場景,一般都沒人考慮這種吧(如有,歡迎補充!!)

4. 先刪 Redis,再寫 MySQL ,再刪 Redis

好,但想要優雅的話,還需要支出額外的維護成本(消息隊列。。)。

5. 先寫 MySQL, 再刪 Redis

推薦,高並發場景適用。

6. 先寫 MySQL,通過 BinLog 異步更新 Redis

說使用,沒試過,但這種對異地容災、數據匯總等場景可能會較好一些。

總結一下

對於實時性要求高的,可以使用“先寫 MySQL, 再刪 Redis”。

對於要求強一致的,可以使用“先寫 MySQL,通過 BinLog 異步更新 Redis”。

其它的之後再補充,這裡就先結束了!

作者:村南

鏈接:
https://juejin.cn/post/7314159614063771682

來源:稀土掘金

#記錄我的2024#