Redis 和 MySQL雙寫一致性的更新策略有哪些?常見面試題深度解答。

目錄

一. 業務數據查詢,更新順序簡要分析

二.?更新數據庫、查詢數據庫、更新緩存、查詢緩存耗時對比

2.1?更新數據庫(最慢)

2.2?查詢數據庫(較慢)

2.3?更新緩存(次快)

2.4?查詢緩存(最快)

三. 數據一致性更新策略舉例說明

3.1 先更新數據庫,再更新緩存

3.2 先更新緩存,再更新數據庫

3.3 先刪除緩存,再更新數據庫

3.4 先更新數據庫,再刪除緩存

3.5 方案對比與選擇

3.5.1 最好先操作數據庫,后操作緩存

3.5.2 最好刪除緩存,而不是更新緩存

3.5.3 具體場景具體分析

四.?低頻修改數據場景 的 推薦解決方案

五.?高頻修改數據場景 的 推薦解決方案

5.1. canal 入門

5.1.1 canal 簡介

5.1.2 canal 下載和配置修改

5.1.3 canal 運行和確認

5.2 MySQL 配置

5.2.1 windows 環境配置修改

5.2.2 windows 環境配置生效驗證

5.2.3 創建 canal 所需要的數據庫權限

5.3?Binlog 監聽 + 消息隊列 流程圖簡要分析

5.4 代碼實例

5.4.1.?業務服務(更新數據庫)

5.4.2.?Canal 客戶端(監聽 Binlog)

5.4.3.?緩存同步服務(刪除緩存 + 失敗入隊)

5.4.4.?消息隊列消費者(重試刪除)

六. 資金賬戶類敏感數據 的 推薦解決方案

七. 面試題合集


Redis,MySQL 雙寫一致性主要是指在使用緩存和數據庫的同時存儲數據時,如果在高并發的場景下,二者可能存在數據不一致的情況,因此希望盡量保證 Redis 中的數據和 MySQL 中的數據盡可能保持一致。

一. 業務數據查詢,更新順序簡要分析

如下圖所示,最左側是我們的 Java 程序,最右側是數據庫MySQL,中間這一層就是緩存 Redis。

在實際業務數據查詢過程中,用戶訪問網站數據,通常會發送查詢請求,通常分為以下三步。

情況一:先查詢 Redis,如果 Redis 有數據,直接返回;

情況二:先查詢 Redis,但是 Redis 無數據,MySQL 有數據,再去查詢MySQL,然后返回數據,同時將數據回寫到 Redis 以便于下次查詢;

情況三:先查詢 Redis,但是 Redis 無數據、再去查詢 MySQL,但是MySQL也沒有數據,返回空。

查詢數據沒什么影響,關鍵在于更新數據操作,如果要更新數據庫,那么緩存也要更新。

這里就會有一個問題?先動緩存還是先動數據庫?緩存時更新緩存較好,還是刪除緩存較好?

由此而來,就引申出了MySQL,redis 更新策略的四種情況。

情況一:先更新數據庫,再更新緩存

情況二:先更新緩存,再更新數據庫

情況三:先刪除緩存,再更新數據庫

情況四:先更新數據庫,再刪除緩存

二.?更新數據庫、查詢數據庫、更新緩存、查詢緩存耗時對比

2.1?更新數據庫(最慢)
  • 操作邏輯:寫入磁盤(如 MySQL 的?UPDATE),需保證 ACID 特性。

  • 耗時范圍毫秒級到秒級(簡單更新約 10~100ms,復雜事務或高并發下更慢)。

  • 關鍵瓶頸

    • 事務提交:需寫事務日志(如 Redo Log)、刷盤(fsync)和同步副本(主從架構)。

    • 鎖開銷:行鎖、間隙鎖等可能阻塞其他操作,尤其在并發場景。

    • 索引維護:更新可能觸發 B+ 樹分裂、索引重建等額外開銷。

2.2?查詢數據庫(較慢)
  • 操作邏輯:從磁盤(如 MySQL)讀取數據,可能涉及索引掃描、鎖等待或復雜查詢。

  • 耗時范圍毫秒級(簡單主鍵查詢約 1~10ms,復雜查詢可達 100ms+)。

  • 關鍵瓶頸

    • 磁盤 I/O:隨機讀性能遠低于內存(機械硬盤約 1ms/次,SSD 約 0.1ms/次)。

    • 鎖競爭:若查詢涉及行鎖或表鎖,可能因事務沖突增加等待時間。

    • 網絡延遲:應用層與數據庫分離時,需疊加網絡 RTT(通常 0.1~1ms)。

2.3?更新緩存(次快)
  • 操作邏輯:寫入內存(如 Redis 的?SET?或?DEL)。

  • 耗時范圍微秒級(Redis 單次寫操作約 0.1~0.5ms)。

  • 關鍵差異

    • 寫操作可能觸發內存分配、序列化或淘汰策略(如 LRU),略慢于讀操作。

    • 若開啟持久化(如 AOF),寫入需追加日志,但通常異步執行,不影響主線程。

2.4?查詢緩存(最快)
  • 操作邏輯:直接從內存(如 Redis)讀取數據,無磁盤 I/O 或復雜計算。

  • 耗時范圍微秒級(Redis 單次讀操作約 0.1ms 內)。

  • 關鍵優勢

    • 內存操作,無物理尋址延遲。

    • 單線程模型(如 Redis)避免鎖競爭,響應穩定。

不難看出,操作緩存的耗時在操作數據庫耗時前幾乎約等于沒有,所以下面我們重點對比線程之間對于數據庫操作的耗時即可,了解了這一點,我們再往下來探究一致性更新策略的對比。
?

三. 數據一致性更新策略舉例說明

常見的四種更新策略,為 先更新數據庫,再更新緩存、先更新緩存,再更新數據庫、先刪除緩存,再更新數據庫、先更新數據庫,再刪除緩存

假設A,B兩個線程同時發起調用。線程A固定為寫操作,線程B可能是讀,也可能是寫操作。

數據庫商品表 product 現在商品數量 number 為100;

3.1 先更新數據庫,再更新緩存

正常邏輯:

(1)A update?mysql = 90

(2)A update redis?= 90

(3)B update mysql = 80

(4)B update redis = 80

多線程情況下,A,B會有快有慢,可能出現如下

異常邏輯:

(1)A update mysql = 90

(2)B update mysql = 80

(3)B update redis = 80

(4)A update redis?= 90

A 更新數據庫,在準備寫入緩存時,B先更新了數據庫,并將數據寫入緩存,然后A又完成了數據庫的更新。最終結果導致MySQL值為80,Redis 值為90,數據不一致。

造成這種情況需要的條件(不考慮網絡延遲):

如果線程B為寫操作,則需要線程A寫入緩存的耗時(0.1~0.5ms)?> 線程B更新數據(10~100ms)+更新緩存的耗時(0.1~0.5ms);

如果線程B為讀操作,則需要線程A寫入緩存的耗時要(0.1~0.5ms) > 線程B查詢緩存的耗時(0.1ms 內);

考慮實際業務中,讀操作往往比寫操作多;總的來說,先更新數據庫,再更新緩存導致數據不一致這種情況大概率會發生。

3.2 先更新緩存,再更新數據庫

正常邏輯:

(1)A update redis?= 90

(2)A update mysql = 90

(3)B update redis = 80

(4)B update mysql = 80

異常邏輯:

(1)A update redis?= 90

(2)B update redis = 80

(3)B update mysql = 80

(4)A update mysql = 90

A 更新緩存,然后更新數據庫,但是在更新數據庫期間,B先來更新了緩存和數據庫,然后A有更新成功了數據庫。最終結果導致MySQL值為 90,Redis 值為80,數據不一致。

造成這種情況需要的條件:

若線程B為寫操作:則需要線程A更新數據庫耗時(10~100ms) > 線程B更新數據庫耗時(10~100ms)+更新緩存耗時(0.1~0.5ms);

若線程B為讀操作:則需要線程A更新數據庫耗時(10~100ms) > 線程B查詢緩存耗時(0.1ms 內);

考慮實際業務中,讀操作往往比寫操作多;總的來說,先更新緩存,再更新數據庫導致數據不一致這種情況大概率會發生

3.3 先刪除緩存,再更新數據庫

多線程:舉例A,B兩個線程同時操作可能出現的問題?

(1)A delete redis 100

(2)B get number from redis,值為空;B get 100 from mysql

(3)B set 100 redis

(4)A update mysql 80

A線程先刪除 redis 的數據,然后再去更新數據庫進行寫操作;

但是由于A的寫操作慢或網絡延遲,導致還未寫成功,B線程來讀數據,發現緩存未命中,又去數據庫讀數據;B在讀取到就數據之后返回,并將舊數據 number 重新寫入 緩存 redis。B操作做完一切之后,A線程完成了寫操作,此時 mysql 的新數據80與 redis 的舊數據100不一致,數據不一致。

我們來分析一下這種情況出現的條件

若線程B為寫請求:需要線程A更新數據庫耗時(10~100ms) >?需要線程B更新數據庫耗時(10~100ms)

若線程B為讀請求:需要線程A更新數據庫耗時(10~100ms) >?需要線程B查詢數據庫耗時(1~10ms) + 線程B寫入緩存耗時(0.1~0.5ms)

考慮實際業務中,讀操作往往比寫操作多;總的來說,先刪除緩存,再更新數據庫導致數據不一致這種情況大概率會發生

3.4 先更新數據庫,再刪除緩存

(1)A update mysql 80

(2)B get 100 from redis

(3)A delete redis

舉例:A線程先去更新數據庫,100變為80,緩存先不動;

然后A再去刪除緩存,但是B來了,讀取到了緩存中的100,直接返回,

A 完成了刪除緩存的操作。?

不難看出,這種情況,在A線程成功刪除緩存之前,也會造成短時間內的臟數據。

我們來分析一下這種情況出現的條件

若線程B為寫請求:需要線程A更新緩存耗時(0.1~0.5ms) >?需要線程B更新數據庫耗時(10~100ms)

若線程B為讀請求:需要線程A刪除緩存耗時(0.1~0.5ms) >?需要線程B查詢緩存耗時(0.1ms 內)

考慮實際業務中,讀操作往往比寫操作多;總的來說,先更新數據庫,再刪除緩存導致數據不一致這種情況大概率會發生

3.5 方案對比與選擇

從上面的四種情況并不難看出,不管我們選擇哪一種,緩存數據不一致的情況大概率都會發生。那么我們到底應該選擇哪一種策略呢?

3.5.1 最好先操作數據庫,后操作緩存

其實就單純數據庫支持事務這一條而言,我們就應該先操作數據庫,因為如果數據庫更新失敗,可以進行事務回滾,或者程序重試。此時我們還尚未操作緩存,不管是更新緩存還是刪除緩存,都還未進行,不會對后續其它讀寫線程造成影響,但如果我們先操作緩存,一旦數據庫更新失敗,就會導致后續其他線程進行緩存重建,浪費時間和性能,做無用功。

下面是先操作數據庫后操作緩存的幾個優點。

(1)降低臟數據風險

  • 若先刪除緩存再更新數據庫,在數據庫更新完成前,若有并發請求查詢數據,會因為緩存缺失讀取數據庫的舊值并重新寫入緩存,導致緩存中保留舊數據。
  • 若先更新數據庫再刪除緩存,即使緩存刪除失敗,緩存中的舊數據也只會短暫存在(下次查詢觸發緩存重建會自動替換為新值),并且數據庫已更新為最新數據,最終一致性可控。

(2)減少不一致的窗口時間

  • 假設更新數據庫為10ms,刪除緩存耗時2ms;
  • 若先刪除緩存再更新數據庫,數據不一致窗口期為10ms(數據庫更新期間);
  • 但若是先更新數據庫再刪除緩存,數據不一致窗口期僅為2ms(刪除緩存期間);?

(3)異常處理的容錯性

  • 數據庫操作通常支持事務,若更新數據庫失敗可以回滾,此時還未刪除緩存不會引入錯誤。
  • 可如果先刪除緩存,但數據庫更新失敗,此時緩存已丟失,后續的請求會穿透到數據庫,并且數據庫未更新成功還會導致緩存重建(額外處理),相對來說耗時間。

(4)避免高并發下的雙寫覆蓋

  • 在高并發場景下,若A線程先刪除緩存,B線程在A線程更新數據庫前查詢舊值并寫入緩存,可能導致緩存與數據庫長期不一致。
  • 但如果先更新數據庫后刪除緩存,即使線程B先讀取到舊值,舊緩存值也會在線程A更新數據庫操作完成后被清除,頂多造成短時間內數據不一致,一旦后續又有新請求,就會觸發緩存重建將新數據寫入緩存。

3.5.2 最好刪除緩存,而不是更新緩存

其實我們也可以用懶加載這一層面來理解這個問題,更新數據庫是要比刪除數據庫更耗費性能的,并且更新的數據不一定會馬上被訪問,既然如此,不如不做,等待其它讀操作在需要的時候再來進行緩存重建即可,這樣既tighao性能,還提高了程序的運行效率。

下面是刪除緩存相對于更新緩存的優點。

(1)避免并發寫入導致臟數據

  • 更新緩存:若線程A更新數據庫后未完成更新緩存,由于時序或網絡延遲,線程B先完成了更新數據庫和緩存,線程A再寫入緩存時,會導致緩存仍是舊值(期望是B修改后的值)
  • 刪除緩存:無論更新順序如何,刪除緩存強制要求下一次查詢加載數據庫的最新值,天然規避了上面線程順序帶來的臟數據影響。

(2)降低計算資源的浪費

  • 更新緩存:每次更新數據庫都需要重新計算并寫入緩存,可能浪費資源(例如數據被頻繁被更新但很少被讀取)
  • 刪除緩存:僅在數據集是被查詢時重建緩存,按需使用資源。

(3)防止部分更新導致的數據不一致

  • 更新緩存:在遇到復雜緩存的數據結構時,例如Hash,List,若只更新部分字段,可能因為代碼的邏輯錯誤或網絡終端導致緩存數據與數據庫不一致。
  • 刪除緩存:直接刪除整個鍵,下次查詢數據時重建完整數據,避免部分更新風險。

(4)簡化異常處理

  • 更新緩存:若緩存更新失敗,需回滾數據庫事務或重試緩存寫入,增加系統復雜性。
  • 刪除緩存:若緩存刪除失敗,可通過異步補償機制(如消息隊列),即使未刪除成功,舊數據也僅短暫存在。

3.5.3 具體場景具體分析

沒有哪個方案是最好的,實際開發過程中都是需要根據項目的實際情況來進行選擇的。

對于數據的操作,無非就是讀和寫。讀操作暫不關心,高頻寫和低頻寫使用的方案一般是略有變差的,如下表格所示。

場景推薦方案一致性級別實現復雜度
低頻修改數據緩存過期 + 延遲雙刪最終一致性
高頻修改數據Binlog 監聽 + 消息隊列最終一致性
資金賬戶類敏感數據

數據庫事務 + 同步更新

(最好使用鎖)

強一致性

下面我們就對這三類場景分別作出分析,并給出一個簡要的代碼邏輯。

四.?低頻修改數據場景 的 推薦解決方案

場景推薦方案一致性級別實現復雜度
低頻修改數據緩存過期 + 延遲雙刪最終一致性

如下示例代碼:

// Redis 產品數量固定字符串 Key 前綴,可有可不有,但標準項目基本都會有 key 前綴,方便管理
public static final String PRODUCT_NUMBER = "PRODUCT:NUMBER:";
// 創建一個固定大小為 4 的線程池
private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(4);
@Transactional
public void updateProductNumber(Long number,String productName){// 拼接查詢 keyString key = PRODUCT_NUMBER + productName;try{// (A1) 線程A首次刪除緩存stringRedisTemplate.delete(key);// 這里要注意,下方查詢緩存重建緩存的邏輯,可能是其他線程(線程B)在線程A更新數據庫之前就已經完成了// (B1) 線程B查詢數據庫String numberStr = stringRedisTemplate.opsForValue().get(key);// (B2):線程B判斷緩存值是否為空if (numberStr == null){// (B3) B線程查詢數據庫Long productnumber = orderMapper.selectProductNumber(productName);numberStr = String.valueOf(productnumber);// (B4) B線程寫入緩存,并給緩存一個過期時間,這里為30分鐘,如果為熱點數據,建議時間更短一些,比如1分鐘,3分鐘,5分鐘等,根據業務需要調整即可if (productnumber != null){stringRedisTemplate.opsForValue().set(key,numberStr,30,TimeUnit.MINUTES);}// (B5) 因為線程B為讀線程,緩存重建后返回數據return ...;}// (A2) 線程A更新數據庫orderMapper.updateProductNumber(number,productName);/* (A3) 線程A 提交事務后開辟新線程異步延遲進行緩存第二次刪除(關鍵!)* A再次刪除緩存,就像上面線程B那樣,因為是查詢,比線程A快,還將緩存重建,* 所以這里進行二次刪除,將線程B寫入到緩存的臟數據刪除掉。  * */asyncExecutor.execute(() -> {try {// 這里的線程是異步的,所以不會阻塞主線程的執行,但是這樣會增加開銷,// 此外,延時時間需根據業務情況測試,我隨便寫的,這里隨便寫為 500ms,需根據業務情況調整。Thread.sleep(500);// (A4) 線程A第二次刪除緩存,如果后續有其它讀請求,就會像上面B線程一樣將緩存重建stringRedisTemplate.delete(key); } catch (InterruptedException e) {Thread.currentThread().interrupt();}});// (A5) 線程A完成操作,返回數據return ...;}catch (Exception e){throw new RuntimeException("程序錯誤",e);}
}

給緩存設計過期時間,定期清理緩存并回寫,是保證數據最終一致性的解決方案

所有的寫操作都要以數據庫為準,對緩存操作只是盡最大努力即可。如果數據庫寫成功,緩存更新失敗,那么只要達到過期時間,則后面的讀請求自然會從數據庫讀取最新的值然后回寫到緩存,達到已執行。總而言之,要以數據庫(MySQL)寫入庫的數據為準。

此種方案比較適合數據修改頻率較低的情況,當然了,每個項目都有對應的特點,結合項目業務特色選擇相應的解決方案即可。

五.?高頻修改數據場景 的 推薦解決方案

場景推薦方案一致性級別實現復雜度
高頻修改數據Binlog 監聽 + 消息隊列最終一致性
5.1. canal 入門
5.1.1 canal 簡介

canal 是阿里巴巴旗下的一款開源項目,基于數據庫增量日志解析,提供增量數據訂閱&消費主要用途是基于 MySQL 數據庫增量日志解析,目前主要支持MySQL。說白了是一個新的技術,第三方中間件,需要額外花時間掌握學習。有興趣的小伙伴可以查閱下面這邊文章,寫的非常好!

【Canal】從原理、配置出發,從0到1完成Canal搭建-CSDN博客

canal 的工作原理類似于將自己偽裝成 MySQL(主機)?的一條從機(slave),然后只要主機數據發生變化,就會同步MySQL主機的數據。想了解主從復制的可以看博主的另一篇文章:

淺談 MySQL 主從復制,優點?原理?_mysql主從優勢-CSDN博客

5.1.2 canal 下載和配置修改

canal 下載:如下圖所示,點擊下載接口

canal.deployer-1.1.6.tar.gz

下載完畢后,解壓得到如下文件,進入 conf——>example——>instance.properties,修改instance.properties 文件。

將 address 改為自己的MySQL地址,下方兩個改為自己的數據庫用戶名和密碼,其它不用動。

5.1.3 canal 運行和確認

OK,保存文件,就改完了,然后?進入 bin 目錄,雙擊運行 startup.bat 腳本運行即可。

然后會出現黑色窗口,我們再單獨開一個 cmd 窗口,輸入

netstat -ano | findstr ":11111"

出現如下就表示運行成功了!

5.2 MySQL 配置
5.2.1 windows 環境配置修改

配置文件路徑
通常位于MySQL安裝目錄下,例如:
C:\Program Files\MySQL\MySQL Server 8.0\my.ini

直接使用文本編輯器打開即可,必須修改的配置項如下

[mysqld]
# 啟用Binlog,指定日志文件名前綴
log-bin=mysql-bin# 設置Binlog格式為ROW(Canal依賴此格式)
binlog_format=ROW# 設置唯一的服務器ID(需確保與其他MySQL實例不沖突)
server-id=1# 可選:設置Binlog保留天數(默認不刪除)
expire_logs_days=7
5.2.2 windows 環境配置生效驗證

更改完畢配置文件后,切記最好重啟MySQL服務。

然后進入 navicat ,運行如下命令確認是否配置成功。

sql
復制
-- 檢查Binlog是否啟用
SHOW VARIABLES LIKE 'log_bin';-- 檢查Binlog格式是否為ROW
SHOW VARIABLES LIKE 'binlog_format';-- 檢查Server ID
SHOW VARIABLES LIKE 'server_id';-- 檢查Binlog保留天數
SHOW VARIABLES LIKE 'expire_logs_days';

5.2.3 創建 canal 所需要的數據庫權限

這里記得換成自己的數據庫密碼!!!

-- 創建用戶(需替換 YOUR_PASSWORD)
CREATE USER 'canal'@'%' IDENTIFIED WITH 'mysql_native_password' BY 'YOUR_PASSWORD';-- 授予復制權限
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';-- 授予 Binlog 訪問權限(MySQL 8.0 必須)
GRANT SELECT, RELOAD, SHOW DATABASES, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'canal'@'%';-- 刷新權限
FLUSH PRIVILEGES;

5.3?Binlog 監聽 + 消息隊列 流程圖簡要分析

流程關鍵點總結

步驟核心目標技術實現
1-2更新數據庫業務代碼直接操作數據庫
3-4解析 BinlogCanal 客戶端監聽并提取 Key
5-6首次刪除緩存獨立服務 + 異常降級到消息隊列
7-8重試保證最終一致性消息隊列異步消費

5.4 代碼實例

在如下代碼中,2,3,4步只需要在項目初期配置好即可,后續就不需要怎么做修改了,頂多在 canal 客戶端添加監聽的數據庫表,此時數據同步代碼基本已經與業務代碼完全解耦合了。

后續我們只需要關注業務層面即可,無需分心關注數據一致性的問題。

5.4.1.?業務服務(更新數據庫)
// ProductService.java
public class ProductService {@Autowiredprivate ProductMapper productMapper;// 步驟1-2:更新數據庫,觸發 Binlog 生成public void updateProductNumber(Long productId, Long productNumber) {// 直接操作數據庫productMapper.updateProductNumber(productId, productNumber);}
}
5.4.2.?Canal 客戶端(監聽 Binlog)

Canal 這里的配置,目前只監聽了 test 數據庫下的 product 表,也可以監聽多張表,或者整個庫,或者跨庫監聽都是可以的。

一般情況下,都是只監聽核心業務表(高頻操作表),這樣不會有冗余數據,并且只監聽幾張表,資源占用較低。

場景示例說明
精確匹配單表test.product只訂閱 test 庫的 product 表
多表逗號分隔test.user,test.product訂閱兩個表
正則表達式匹配test\\..*訂閱test庫所有表
跨庫匹配db1\\..*,db2.order_.*訂閱db1所有表和db2的order前綴表

通常情況下,canal 還需要在 properties 或 yml 文件中進行配置。?

# Canal 連接 MySQL 的配置
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=YOUR_PASSWORD
canal.instance.connectionCharset=UTF-8
// CanalClient.java
public class CanalClient {public static void main(String[] args) {// 連接 Canal 服務端CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1", 11111), "example", "", "");connector.connect();connector.subscribe("test.product"); // 訂閱 product 表的 Binlogwhile (true) {Message message = connector.getWithoutAck(100); // 拉取 Binlogfor (CanalEntry.Entry entry : message.getEntries()) {if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {// 步驟3-4:解析 Binlog,提取 KeyRowChange rowChange = RowChange.parseFrom(entry.getStoreValue());for (RowData rowData : rowChange.getRowDatasList()) {Long userId = rowData.getAfterColumnsList().get(0).getValue();String key = "user:" + userId;// 調用緩存同步服務CacheSyncService.process(key);}}}connector.ack(message.getId()); // 確認消費}}
}
5.4.3.?緩存同步服務(刪除緩存 + 失敗入隊)
// CacheSyncService.java
public class CacheSyncService {private static RedisClient redis = new RedisClient("redis://localhost:6379");private static MessageQueue mq = new KafkaMessageQueue("kafka:9092");// 步驟5-6:嘗試刪除緩存,失敗則發送到消息隊列public static void process(String key) {try {boolean success = redis.delete(key);if (!success) {mq.send("cache_retry_queue", key); // 發送到重試隊列}} catch (Exception e) {mq.send("cache_retry_queue", key); // 異常時也發送}}
}
5.4.4.?消息隊列消費者(重試刪除)
// MQConsumer.java
public class MQConsumer {public static void main(String[] args) {MessageQueue mq = new KafkaMessageQueue("kafka:9092");RedisClient redis = new RedisClient("redis://localhost:6379");// 步驟7-8:訂閱隊列并重試刪除mq.subscribe("cache_retry_queue", message -> {String key = (String) message;try {boolean success = redis.delete(key);if (!success) {System.err.println("重試刪除失敗: " + key);// 可添加重試次數限制(例如最多重試3次)}} catch (Exception e) {mq.send("cache_retry_queue", key); // 再次入隊}});}
}

六. 資金賬戶類敏感數據 的 推薦解決方案

場景推薦方案一致性級別實現復雜度
資金賬戶類敏感數據

數據庫事務 + 同步更新

(最好使用鎖)

強一致性

示例代碼如下:

沒啥可說的,就是使用了分布式鎖,強制線程串行化執行,基本不存在并發導致數據不一致的情況發生。

private final AccountMapper accountMapper;
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;@Transactional(rollbackFor = Exception.class)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {RLock lock = redissonClient.getLock("account_lock:" + fromId + ":" + toId);try {if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {// MyBatis查詢Account fromAccount = accountMapper.selectById(fromId);Account toAccount = accountMapper.selectById(toId);// 余額計算fromAccount.setBalance(fromAccount.getBalance().subtract(amount));toAccount.setBalance(toAccount.getBalance().add(amount));// MyBatis更新accountMapper.updateBalance(fromAccount);accountMapper.updateBalance(toAccount);// 使用StringRedisTemplate存儲JSONObjectMapper objectMapper = new ObjectMapper();stringRedisTemplate.opsForValue().set("account:" + fromId,objectMapper.writeValueAsString(fromAccount));stringRedisTemplate.opsForValue().set("account:" + toId,objectMapper.writeValueAsString(toAccount));}} finally {lock.unlock();}
}

七. 面試題合集

問題一:有這樣一種情況,微服務查詢 Redis 無數據,MySQL 有數據,為保證數據雙寫一致性再回寫到 Redis 時,需要注意什么?雙檢加鎖策略了解過嗎?

雙檢加鎖其實和延時雙刪思路是一樣的,簡單來說。在高并發的情況下,如果線程A查詢緩存,無數據,然后會查詢數據庫,發現有數據然后將數據回寫到緩存,但在查詢數據庫期間,可能已經有其他線程(線程B)先一步完成了查詢數據庫并回寫了緩存,此時A再回寫緩存已經無意義了。所以在線程A查詢到數據準備回寫緩存之前,可以再進行一次判斷,查看當前緩存中是否依舊為空,如果為空說明緩存還未被重建,則再去回寫緩存。

示例代碼如下:

public static final String USER_PREFIX = "USER:";
public User findUserById(Long id){// 拼接Redis的key,用戶對象,緩存用戶字符串對象;String key = USER_PREFIX + id;User user = null;String userStr = null;try {// 1. 從Redis中查詢用戶信息:若結果不為空,轉化后直接返回//                        若結果為空,再去查詢數據庫userStr = stringRedisTemplate.opsForValue().get(key);if (userStr != null) {return JSONUtil.toBean(userStr, User.class);}else {/** 2. 拿到鎖之后,再次查詢緩存確保緩存無數據,雙重檢查,俗稱雙檢* 之做所以這樣做,是因為在高并發情況下,可能會存在多個線程同時進入,導致緩存已經重建,從而導致數據庫被查詢多次* 會對數據庫服務器造成壓力**/userStr = stringRedisTemplate.opsForValue().get(key);// 3. 判斷是否為空,如果不為空,說明緩存已被其他線程重建,直接返回數據//                如果為空,進行緩存重建,從數據庫中查詢數據,然后重建緩存if (userStr != null) {return JSONUtil.toBean(userStr, User.class);} else {// 4. 從數據庫中查詢用戶信息user = userMapper.selectById(id);if (user == null){// 如果是一個熱點key,應該回寫到redis里一個空值,避免緩存穿透,時間根據業務需求自己定,我隨便寫的stringRedisTemplate.opsForValue().set(key, "", 1, TimeUnit.MINUTES);return null;}// 5. 將對象轉為jsonuserStr = JSONUtil.toJsonStr(user);// 6. 寫入RedisstringRedisTemplate.opsForValue().set(key, userStr, 3, TimeUnit.DAYS);}}} catch (Exception e) {logger.error("程序發生錯誤失敗", e);}return user;
}

下面這四個問題,答案全都已經在文章中了,就留給小伙伴們自行解答啦!?

問題二:只要使用緩存,就可能涉及到 Redis 緩存與數據庫雙存儲雙寫,只要有雙寫,就一定有數據一致性問題,如何解決數據一致性問題?

問題三:雙寫一致性,先動緩存 Redis 還是先動數據庫 MySQL ?原因是什么?

問題四:延時雙刪了解過嗎?怎么做?

問題五:Redis 和 MySQL雙寫一定會出現紕漏,雖然做不到強一致性,但可以做到最終一致性,怎么做?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/75834.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/75834.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/75834.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SRT協議

SRT(Secure Reliable Transport)是一種開源的視頻傳輸協議,專為高丟包、高延遲網絡環境設計,結合了UDP的低延遲和TCP的可靠性,廣泛應用于直播、遠程制作、視頻會議等場景。 定位:SRT協議的官方C/C實現庫&am…

“征服HTML引號惡魔:“完全解析手冊”!!!(quot;表示雙引號)

&#x1f6a8;&#x1f4e2; "征服HTML引號惡魔&#xff1a;“完全解析手冊” &#x1f4e2;&#x1f6a8; &#x1f3af; 博客引言&#xff1a;當引號變成"惡魔" &#x1f631; 是否遇到過這種情況&#xff1a; 寫HTML時滿心歡喜輸入<div title"他…

npm install 卡在創建項目:sill idealTree buildDeps

參考&#xff1a; https://blog.csdn.net/PengXing_Huang/article/details/136460133 或者再執行 npm install -g cnpm --registryhttps://registry.npm.taobao.org 或者換梯子

c++中cpp文件從編譯到執行的過程

C 文件從編寫到執行的過程可以分為幾個主要階段&#xff1a;編寫代碼、預處理、編譯、匯編、鏈接和運行。以下是每個階段的詳細說明&#xff1a; 1. 編寫代碼 這是整個過程的起點。程序員使用文本編輯器&#xff08;如 VSCode、Sublime Text 或其他 IDE&#xff09;編寫 C 源…

PROE 與 STL 格式轉換:開啟 3D 打印及多元應用的大門

在 3D 設計與制造的復雜生態中&#xff0c;將 PROE 格式轉換為 STL 格式絕非無端之舉&#xff0c;而是有著深厚且多元的現實需求作為支撐。 一、文件格式介紹? &#xff08;一&#xff09;PROE 格式? PROE 作為一款參數化設計軟件&#xff0c;采用基于特征的參數化建模技術…

開發中后端返回下劃線數據,要不要統一轉駝峰?

先說結論。看情況&#xff01;&#xff01;&#xff01;&#xff01; 前端 主要用 JS/TS 建議后端返回 camelCase&#xff0c;減少前端轉換成本。后端 主要是 Python/Go 建議保持 snake_case&#xff0c;前端做轉換。但是團隊統一風格最重要&#xff01;如果統一返回駝峰就駝峰…

docker pull時報錯:https://registry-1.docker.io/v2/

原文&#xff1a;https://www.cnblogs.com/sdgtxuyong/p/18647915 https://www.cnblogs.com/OneSeting/p/18532166 docker 換源&#xff0c;解決連接不上的問題。 編輯以下文件&#xff0c;不存在則創建&#xff1a; vim /etc/docker/daemon.json {"registry-mirrors&qu…

Pytorch學習筆記(十二)Learning PyTorch - NLP from Scratch

這篇博客瞄準的是 pytorch 官方教程中 Learning PyTorch 章節的 NLP from Scratch 部分。 官網鏈接&#xff1a;https://pytorch.org/tutorials/intermediate/nlp_from_scratch_index.html 完整網盤鏈接: https://pan.baidu.com/s/1L9PVZ-KRDGVER-AJnXOvlQ?pwdaa2m 提取碼: …

基礎算法02——冒泡排序(Bubble Sort)

冒泡排序&#xff08;Bubble Sort&#xff09; 冒泡排序&#xff1a;是一種簡單的排序算法&#xff0c;其基本思想是通過重復遍歷要排序的列表&#xff0c;比較相鄰的元素&#xff0c;并在必要時&#xff08;即前面的數比后面的數大的時候&#xff09;交換它們的位置&#xff…

RestTemplate遠程調用接口方式

1.Post(body空參) 也就是說需要給一個空的json 代碼: String getDeviceUrl this.MOVABLE_URL "detected-data/getMachineLists"; // 遠程調用 RestTemplate restTemplate new RestTemplate(); restTemplate.getMessageConverters().set(1,new StringHttpMessageC…

ar頭顯和眼鏡圖像特效處理

使用一個線程從攝像頭或者其他設備循環讀取圖像數據寫入鏈表&#xff0c;另一個線程從鏈表循環讀取數據并做相應的特效處理&#xff0c;由于寫入的速度比讀取的快&#xff0c;最終必然會因為寫入過快導致線程讀寫一幀而引發沖突和數據幀正常數據幀被覆蓋。最好使用共享內存&…

mysql--socket報錯

錯誤原因分析 MySQL 服務未運行&#xff08;最常見原因&#xff09; 錯誤中的 (2) 表示 “No such file or directory”&#xff0c;即 /tmp/mysql.sock 不存在這通常意味著 MySQL 服務器根本沒有啟動 socket 文件路徑不匹配 客戶端嘗試連接 /tmp/mysql.sock但 MySQL 服務器可…

labview加載matlab數據時報錯提示:對象引用句柄無效。

1. labview報錯提示 labview加載mat數據時報錯提示&#xff1a;對象引用句柄無效。返回該引用句柄的節點可能遇到錯誤&#xff0c;并沒有返回有效的引用句柄。該引用句柄所指的存儲可能在執行調用之前已關閉。報錯提示如下&#xff1a; 這是由于labview缺少matlab MathWorks導…

面試計算機操作系統解析(一中)

判斷 1. 一般來說&#xff0c;先進先出頁面置換算法比最近最少使用頁面置換算法有較少的缺頁率。&#xff08;?&#xff09; 正確答案&#xff1a;錯誤解釋&#xff1a;FIFO&#xff08;先進先出&#xff09;頁面置換算法可能導致“Belady異常”&#xff0c;即頁面數增加反而…

如何防御TCP洪泛攻擊

TCP洪泛攻擊&#xff08;TCP Flood Attack&#xff09;是一種常見的分布式拒絕服務&#xff08;DDoS&#xff09;攻擊手段&#xff0c;以下是其原理、攻擊方式和危害的詳細介紹&#xff1a; 定義與原理 TCP洪泛攻擊利用了TCP協議的三次握手過程。在正常的TCP連接建立過程中&a…

20250330 Pyflink with Paimon

1. 數據湖 2. 本地安裝Pyflink和Paimon 必須安裝Python 3.11 Pip install python -m pip install apache-flink1.20.1 需要手動加入這兩個jar 測試代碼&#xff1a; import argparse import logging import sys import timefrom pyflink.common import Row from pyflink.tab…

-PHP 應用SQL 盲注布爾回顯延時判斷報錯處理增刪改查方式

#PHP-MYSQL-SQL 操作 - 增刪改查 1 、功能&#xff1a;數據查詢(對數據感興趣&#xff09; 查詢&#xff1a; SELECT * FROM news where id$id 2 、功能&#xff1a;新增用戶&#xff0c;添加新聞等&#xff08;對操作的結果感興趣&#xff09; 增加&#xff1a; INSERT INT…

【學習記錄】大模型微調之使用 LLaMA-Factory 微調 Qwen系列大模型,可以用自己的數據訓練

一、LoRA微調的基本原理 1、基本概念 LoRA&#xff08;Low-Rank Adaptation&#xff09;是一種用于大模型微調的技術&#xff0c;通過引入低秩矩陣來減少微調時的參數量。在預訓練的模型中&#xff0c;LoRA通過添加兩個小矩陣B和A來近似原始的大矩陣ΔW&#xff0c;從而減少需…

Vue 使用 xlsx 插件導出 excel 文件

安裝與引入 安裝 npm install xlsx npm install file-saver # 或者 yarn add xlsx yarn add file-saver 引入 import * as XLSX from xlsx; import FileSaver from file-saver 基本功能 讀取 Excel 文件 // 讀取文件內容 const workbook XLSX.readFile(path/to/file.xl…

vulntarget_a 訓練筆記

win 7 權限 利用任意文件上傳 getshell POST /module/ueditor/php/action_upload.php?actionuploadfile HTTP/1.1 User-Agent: Mozilla/5.0 (compatible; Baiduspider/2.0; http://www.baidu.com/search/spider.html) Accept: */* Accept-Language: zh-CN,zh;q0.9 Connectio…