最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MySQL鎖等待與死鎖根治的全攻略

 更新時間:2026年04月09日 08:37:20   作者:殷紫川  
本文詳細(xì)分析了MySQL中的鎖等待和死鎖問題,解釋了InnoDB鎖的基本概念、分類以及它們之間的兼容性,并提供了排查這些鎖相關(guān)問題的方法,重點關(guān)注了鎖等待的常見原因及對應(yīng)的解決策略,需要的朋友可以參考下

線上業(yè)務(wù)突然拋出Lock wait timeout exceeded異常、接口超時、甚至事務(wù)被強制回滾,90%以上的場景都源于MySQL鎖等待與死鎖問題。很多開發(fā)者面對問題時無從下手,要么只會重啟服務(wù),要么找不到根因?qū)е聠栴}反復(fù)出現(xiàn)。

一、InnoDB鎖體系核心前置認(rèn)知

想要排查鎖問題,必須先搞懂InnoDB鎖的底層邏輯,90%的鎖問題排查失敗,都源于對鎖類型、兼容性、生效規(guī)則的認(rèn)知錯誤。

1.1 鎖的核心維度劃分

1.1 鎖的核心維度劃分

1.1.1 按兼容性劃分的基礎(chǔ)鎖類型

這是鎖機制的核心基礎(chǔ),兼容性矩陣決定了鎖是否會發(fā)生阻塞,所有規(guī)則100%遵循MySQL 8.0官方規(guī)范

鎖類型共享鎖(S)排他鎖(X)意向共享鎖(IS)意向排他鎖(IX)
共享鎖(S)兼容互斥兼容互斥
排他鎖(X)互斥互斥互斥互斥
意向共享鎖(IS)兼容互斥兼容兼容
意向排他鎖(IX)互斥互斥兼容兼容
  • 共享鎖(S鎖) :讀鎖,多個事務(wù)可同時持有同一行的S鎖,用于select ... lock in share mode,持有S鎖的事務(wù)只能讀不能修改該行。
  • 排他鎖(X鎖) :寫鎖,一個事務(wù)持有某行的X鎖后,其他事務(wù)不能持有該行的任何鎖,UPDATE/DELETE/INSERT語句會自動加X鎖,select ... for update手動加X鎖。
  • 意向鎖(IS/IX) :表級鎖,用于快速判斷表內(nèi)是否有行鎖,避免全表掃描檢查行鎖,事務(wù)加行鎖前必須先加對應(yīng)的意向鎖,意向鎖之間互相兼容,僅與表級的S/X鎖互斥。

1.1.2 行級鎖的細(xì)分類型(RR隔離級別,MySQL 8.0默認(rèn))

這是死鎖問題的核心重災(zāi)區(qū),必須精準(zhǔn)理解每一種鎖的生效規(guī)則:

  1. 記錄鎖(Record Lock) :僅鎖住索引中的某一行具體記錄,只在唯一索引+精準(zhǔn)匹配的場景下生效,比如where id=1(id為主鍵),僅鎖住id=1的行,對其他行完全無影響。
  2. 間隙鎖(Gap Lock) :鎖住索引記錄之間的間隙,僅用于防止幻讀,核心規(guī)則:間隙鎖之間完全兼容,僅與插入意向鎖互斥。比如表中id有1、3、5,間隙分為(-∞,1)、(1,3)、(3,5)、(5,+∞),執(zhí)行select * from t where id=2 for update會鎖住(1,3)間隙,禁止任何插入該區(qū)間的操作,但其他事務(wù)可以同時持有該間隙的間隙鎖。
  3. 臨鍵鎖(Next-Key Lock) :InnoDB RR級別下默認(rèn)的行鎖算法,由「記錄鎖+該記錄左邊的間隙鎖」組成,左開右閉區(qū)間,比如上述id的臨鍵鎖區(qū)間為(-∞,1]、(1,3]、(3,5]、(5,+∞]。僅當(dāng)查詢條件為唯一索引+精準(zhǔn)匹配時,臨鍵鎖會退化為記錄鎖,其他場景均為臨鍵鎖。
  4. 插入意向鎖(Insert Intention Lock) :一種特殊的間隙鎖,INSERT語句插入前會先申請該鎖,它與間隙鎖互斥,與其他插入意向鎖兼容,是間隙鎖場景下死鎖的核心誘因。

1.2 鎖等待與死鎖的核心區(qū)別

很多開發(fā)者會將兩者混為一談,實際上二者的觸發(fā)邏輯、處理機制完全不同:

二、鎖等待的根因拆解與全鏈路排查方法 論

2.1 鎖等待的高頻根因

  1. 長事務(wù)未提交占用行鎖:事務(wù)執(zhí)行完更新語句后未及時提交/回滾,長期持有行鎖,導(dǎo)致后續(xù)所有操作該行的事務(wù)全部阻塞,是線上最常見的鎖等待誘因。
  2. 索引失效導(dǎo)致行鎖升級為全表鎖:更新/刪除語句的查詢條件沒有索引,或索引失效,InnoDB無法精準(zhǔn)定位行,會走全表掃描并給所有行加臨鍵鎖,相當(dāng)于鎖全表,任何更新操作都會被阻塞。
  3. 熱點行并發(fā)更新:秒殺、庫存扣減等場景,大量并發(fā)請求同時更新同一行記錄,導(dǎo)致后續(xù)事務(wù)排隊等待鎖釋放,出現(xiàn)大面積鎖等待超時。
  4. 手動加鎖范圍過大:濫用select ... for update,查詢條件范圍過大或無索引,導(dǎo)致鎖住大量無關(guān)行,阻塞其他業(yè)務(wù)操作。

2.2 鎖等待的標(biāo)準(zhǔn)化排查步驟

MySQL 8.0.13之后,鎖信息從information_schema.INNODB_LOCKS遷移至performance_schema.data_locks,以下SQL均適配MySQL 8.0最新規(guī)范。

步驟1:定位阻塞的線程與SQL

執(zhí)行以下命令,找到處于鎖等待狀態(tài)的線程:

show processlist;

重點關(guān)注State列值為Waiting for row lock/Waiting for table metadata lock的線程,記錄Id(MySQL線程ID)、Time(阻塞時長)、Info(正在執(zhí)行的SQL)。

步驟2:查詢鎖等待的關(guān)聯(lián)關(guān)系

執(zhí)行以下SQL,直接定位「等待鎖的事務(wù)」與「持有鎖的事務(wù)」的對應(yīng)關(guān)系:

SELECT
  r.trx_id waiting_trx_id,
  r.trx_mysql_thread_id waiting_thread_id,
  r.trx_query waiting_sql,
  b.trx_id blocking_trx_id,
  b.trx_mysql_thread_id blocking_thread_id,
  b.trx_query blocking_sql,
  b.trx_started blocking_trx_start_time
FROM
  performance_schema.data_lock_waits w
  INNER JOIN information_schema.innodb_trx b ON w.blocking_engine_transaction_id = b.trx_id
  INNER JOIN information_schema.innodb_trx r ON w.requesting_engine_transaction_id = r.trx_id;

步驟3:查看鎖的詳細(xì)信息

執(zhí)行以下SQL,查看當(dāng)前數(shù)據(jù)庫中所有持有的鎖詳情,包括鎖類型、鎖所在的表、索引、具體記錄:

SELECT
  engine_transaction_id trx_id,
  object_schema db_name,
  object_name table_name,
  index_name,
  lock_type,
  lock_mode,
  lock_data,
  lock_status
FROM
  performance_schema.data_locks;

步驟4:根因定位與應(yīng)急處理

  1. 若阻塞源是未提交的長事務(wù),可通過kill 阻塞線程ID臨時釋放鎖,恢復(fù)業(yè)務(wù);
  2. 若為索引失效導(dǎo)致的全表鎖,立即給查詢條件添加合適的索引,確保更新語句走精準(zhǔn)索引;
  3. 若為熱點行更新,優(yōu)化業(yè)務(wù)邏輯,采用分布式鎖、隊列削峰等方式控制并發(fā)度。

三、死鎖的形成條件與高頻場景復(fù)現(xiàn)

3.1 死鎖形成的4個必要條件

死鎖的觸發(fā)必須同時滿足以下4個條件,只要打破其中任意一個,就能徹底避免死鎖

  1. 互斥條件:鎖只能被一個事務(wù)持有,其他事務(wù)無法同時持有同一把鎖;
  2. 占有且等待:事務(wù)已持有至少一把鎖,又申請新的鎖,且新鎖被其他事務(wù)持有時,不釋放已持有的鎖;
  3. 不可搶占:事務(wù)已持有的鎖,只能由自身提交/回滾釋放,無法被其他事務(wù)強制搶占;
  4. 循環(huán)等待:多個事務(wù)形成頭尾相接的等待閉環(huán),互相等待對方持有的鎖。

3.2 線上高頻死鎖場景與完整復(fù)現(xiàn)

以下所有場景均基于MySQL 8.0默認(rèn)RR隔離級別,SQL可直接執(zhí)行復(fù)現(xiàn)。

場景1:交叉更新行導(dǎo)致的死鎖(最經(jīng)典、最高發(fā))

表結(jié)構(gòu)與初始化數(shù)據(jù)

CREATE TABLE `t_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `user_name` varchar(64) NOT NULL COMMENT '用戶名',
  `age` int NOT NULL COMMENT '年齡',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';
INSERT INTO `t_user` (`id`, `user_name`, `age`) VALUES (1, '張三', 20), (2, '李四', 25);

死鎖復(fù)現(xiàn)步驟(按時間順序執(zhí)行)

時間事務(wù)A事務(wù)B
T1BEGIN;BEGIN;
T2UPDATE t_user SET age=21 WHERE id=1;UPDATE t_user SET age=26 WHERE id=2;
T3UPDATE t_user SET age=22 WHERE id=2;
T4阻塞,等待id=2的行X鎖UPDATE t_user SET age=27 WHERE id=1;
T5死鎖觸發(fā),事務(wù)被回滾

根因分析:事務(wù)A持有id=1的X鎖,申請id=2的X鎖;事務(wù)B持有id=2的X鎖,申請id=1的X鎖,形成循環(huán)等待閉環(huán),4個死鎖條件全部滿足,觸發(fā)死鎖。

場景2:間隙鎖+插入意向鎖導(dǎo)致的死鎖(90%線上隱藏死鎖的誘因)

表結(jié)構(gòu)復(fù)用上述t_user表,當(dāng)前數(shù)據(jù)id為1、2,存在間隙(2,+∞)

死鎖復(fù)現(xiàn)步驟(按時間順序執(zhí)行)

時間事務(wù)A事務(wù)B
T1BEGIN;BEGIN;
T2SELECT * FROM t_user WHERE id=3 FOR UPDATE;SELECT * FROM t_user WHERE id=4 FOR UPDATE;
T3持有(2,+∞)的間隙鎖持有(2,+∞)的間隙鎖(間隙鎖之間兼容,無阻塞)
T4INSERT INTO t_user (id, user_name, age) VALUES (3, '王五', 30);
T5阻塞,申請插入意向鎖,被事務(wù)B的間隙鎖阻塞INSERT INTO t_user (id, user_name, age) VALUES (4, '趙六', 35);
T6阻塞,申請插入意向鎖,被事務(wù)A的間隙鎖阻塞,死鎖觸發(fā)

根因分析:兩個事務(wù)同時持有同一間隙的間隙鎖,又同時申請插入意向鎖,而插入意向鎖與間隙鎖互斥,形成循環(huán)等待,觸發(fā)死鎖。該場景是線上最高發(fā)的隱藏死鎖,很多開發(fā)者因不了解間隙鎖的兼容規(guī)則,無法定位根因。

場景3:唯一鍵沖突導(dǎo)致的死鎖

表結(jié)構(gòu)復(fù)用上述t_user表,user_name為唯一索引

死鎖復(fù)現(xiàn)步驟(按時間順序執(zhí)行)

時間事務(wù)A事務(wù)B事務(wù)C
T1BEGIN;BEGIN;BEGIN;
T2INSERT INTO t_user (user_name, age) VALUES ('test', 18);
T3INSERT INTO t_user (user_name, age) VALUES ('test', 18);INSERT INTO t_user (user_name, age) VALUES ('test', 18);
T4唯一鍵沖突,阻塞,申請S鎖唯一鍵沖突,阻塞,申請S鎖
T5ROLLBACK;
T6事務(wù)A釋放鎖,B和C同時拿到S鎖持有S鎖,申請X鎖完成插入持有S鎖,申請X鎖完成插入
T7阻塞,等待C的S鎖釋放阻塞,等待B的S鎖釋放,死鎖觸發(fā)

根因分析:唯一鍵沖突時,InnoDB不會直接報錯,而是會給沖突的記錄申請S鎖;事務(wù)A回滾后,B和C同時持有S鎖,又同時申請X鎖,X鎖與S鎖互斥,形成循環(huán)等待,觸發(fā)死鎖。

四、show engine innodb status 死鎖日志完整解讀與精準(zhǔn)定位

show engine innodb status是MySQL排查死鎖的終極工具,它會記錄最近一次死鎖的完整信息,包含參與事務(wù)的SQL、持有的鎖、等待的鎖、循環(huán)等待鏈條等核心信息,只要讀懂這份日志,就能100%定位死鎖源頭。

4.1 死鎖日志完整示例

以下是上述場景1交叉更新觸發(fā)的完整死鎖日志,來自MySQL 8.0環(huán)境:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-04-08 10:00:00 0x7f8a1b2c3700
*** (1) TRANSACTION:
TRANSACTION 42107, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 12, OS thread handle 140230523201280, query id 245 localhost root updating
UPDATE t_user SET age=22 WHERE id=2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`t_user` trx id 42107 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
 1: len 6; hex 00000000a47b; asc      {;;
 2: len 7; hex 81000001100110; asc        ;;
 3: len 4; hex 8000001a; asc     ;;
 4: len 2; hex e69d8e; asc   ;;

*** (2) TRANSACTION:
TRANSACTION 42108, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 13, OS thread handle 140230522930944, query id 246 localhost root updating
UPDATE t_user SET age=27 WHERE id=1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`t_user` trx id 42108 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
 1: len 6; hex 00000000a47b; asc      {;;
 2: len 7; hex 81000001100110; asc        ;;
 3: len 4; hex 8000001a; asc     ;;
 4: len 2; hex e69d8e; asc   ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`t_user` trx id 42108 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;
 1: len 6; hex 00000000a47a; asc      z;;
 2: len 7; hex 820000010f0120; asc         ;;
 3: len 4; hex 80000014; asc     ;;
 4: len 3; hex e5bca0e4b889; asc       ;;

*** WE ROLL BACK TRANSACTION (1)

4.2 死鎖日志逐行精準(zhǔn)解讀

1. 死鎖頭部信息

2026-04-08 10:00:00 0x7f8a1b2c3700
  • 第一部分:死鎖發(fā)生的精確時間;
  • 第二部分:觸發(fā)死鎖的InnoDB OS線程ID,可用于關(guān)聯(lián)MySQL錯誤日志。

2. 第一個參與死鎖的事務(wù)(事務(wù)1)

*** (1) TRANSACTION:
TRANSACTION 42107, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 12, OS thread handle 140230523201280, query id 245 localhost root updating
UPDATE t_user SET age=22 WHERE id=2
  • TRANSACTION 42107:事務(wù)的唯一ID,可用于關(guān)聯(lián)事務(wù)詳情表;
  • ACTIVE 10 sec:事務(wù)已活躍10秒,鎖持有時間過長是死鎖的重要誘因;
  • LOCK WAIT:當(dāng)前事務(wù)處于鎖等待狀態(tài);
  • MySQL thread id 12:MySQL線程ID,可通過kill 12終止該線程;
  • 最后一行:事務(wù)當(dāng)前正在執(zhí)行的、被阻塞的SQL語句,可直接定位到業(yè)務(wù)代碼位置。

3. 事務(wù)1等待的鎖信息(核心定位點)

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`t_user` trx id 42107 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
  • RECORD LOCKS:鎖類型為記錄鎖;
  • index PRIMARY:鎖加在主鍵索引上,可直接定位到鎖對應(yīng)的索引;
  • table test.t_user:鎖對應(yīng)的庫名和表名;
  • lock_mode X locks rec but not gap waiting:鎖模式為排他鎖(X),僅鎖記錄不鎖間隙,當(dāng)前處于等待狀態(tài);
  • 0: len 8; hex 8000000000000002:主鍵索引的字段值,十六進制轉(zhuǎn)換為十進制為2,即事務(wù)1正在等待id=2的主鍵記錄的X鎖。

4. 第二個參與死鎖的事務(wù)(事務(wù)2)

結(jié)構(gòu)與事務(wù)1完全一致,核心信息為:事務(wù)ID為42108,MySQL線程ID為13,正在執(zhí)行的SQL為UPDATE t_user SET age=27 WHERE id=1。

5. 事務(wù)2持有的鎖信息(核心關(guān)聯(lián)點)

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`t_user` trx id 42108 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;

該部分明確顯示:事務(wù)2持有test.t_user表主鍵索引上id=2的記錄的X鎖,正好是事務(wù)1正在等待的鎖。

6. 事務(wù)2等待的鎖信息

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`t_user` trx id 42108 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;

該部分明確顯示:事務(wù)2正在等待test.t_user表主鍵索引上id=1的記錄的X鎖,而該鎖正好被事務(wù)1持有。

7. 死鎖處理結(jié)果

*** WE ROLL BACK TRANSACTION (1)

InnoDB死鎖檢測線程會計算兩個事務(wù)的回滾代價,選擇代價最小的事務(wù)進行回滾,此處回滾了僅持有1個行鎖的事務(wù)1。

4.3 鎖模式關(guān)鍵字段解讀

日志中鎖模式的關(guān)鍵字段直接決定了死鎖的類型,必須精準(zhǔn)理解:

字段含義
lock_mode X排他鎖
lock_mode S共享鎖
locks rec but not gap僅記錄鎖,無間隙鎖
gap僅間隙鎖,不鎖記錄
locks gap before rec臨鍵鎖(記錄鎖+左邊間隙鎖)
insert intention插入意向鎖
waiting正在等待該鎖

五、死鎖全鏈路實戰(zhàn)排查與修復(fù)

以下通過Java業(yè)務(wù)代碼復(fù)現(xiàn)死鎖,再通過上述方法定位根因,最終給出修復(fù)方案,形成完整的排查閉環(huán)。

5.1 項目環(huán)境與核心代碼

項目基于JDK 17、Spring Boot 3.2.5、MyBatis-Plus 3.5.7開發(fā)。

pom.xml核心依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    <groupId>com.jam.demo</groupId>
    <artifactId>mysql-lock-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mysql-lock-demo</name>
    <description>MySQL鎖與死鎖實戰(zhàn)Demo</description>
    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <fastjson2.version>2.0.52</fastjson2.version>
        <guava.version>33.1.0-jre</guava.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

實體類User

package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
 * 用戶實體類
 * @author ken
 */
@Data
@TableName("t_user")
@Schema(description = "用戶實體")
public class User implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    @TableId(type = IdType.AUTO)
    @Schema(description = "主鍵ID", example = "1")
    private Long id;
    @Schema(description = "用戶名", example = "張三")
    private String userName;
    @Schema(description = "年齡", example = "20")
    private Integer age;
}

Mapper接口UserMapper

package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
 * 用戶Mapper接口
 * @author ken
 */
public interface UserMapper extends BaseMapper<User> {
    /**
     * 根據(jù)ID更新用戶年齡
     * @param id 用戶ID
     * @param age 新年齡
     * @return 影響行數(shù)
     */
    @Update("UPDATE t_user SET age = #{age} WHERE id = #{id}")
    int updateAgeById(@Param("id") Long id, @Param("age") Integer age);
}

服務(wù)實現(xiàn)類UserServiceImpl

package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;
import jakarta.annotation.Resource;
/**
 * 用戶服務(wù)實現(xiàn)類
 * @author ken
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Resource
    private PlatformTransactionManager transactionManager;
    @Resource
    private UserMapper userMapper;
    @Override
    public void crossUpdateFirst(Long id1, Long id2, Integer age1, Integer age2) {
        if (ObjectUtils.isEmpty(id1) || ObjectUtils.isEmpty(id2)) {
            throw new IllegalArgumentException("用戶ID不能為空");
        }
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            log.info("事務(wù)A:開始更新id={}的用戶年齡", id1);
            userMapper.updateAgeById(id1, age1);
            Thread.sleep(1000);
            log.info("事務(wù)A:開始更新id={}的用戶年齡", id2);
            userMapper.updateAgeById(id2, age2);
            transactionManager.commit(status);
            log.info("事務(wù)A:執(zhí)行完成,事務(wù)提交");
        } catch (InterruptedException e) {
            transactionManager.rollback(status);
            Thread.currentThread().interrupt();
            log.error("事務(wù)A:線程中斷,事務(wù)回滾", e);
            throw new RuntimeException("更新失敗,線程中斷", e);
        } catch (Exception e) {
            transactionManager.rollback(status);
            log.error("事務(wù)A:更新異常,事務(wù)回滾", e);
            throw new RuntimeException("更新失敗", e);
        }
    }
    @Override
    public void crossUpdateSecond(Long id1, Long id2, Integer age1, Integer age2) {
        if (ObjectUtils.isEmpty(id1) || ObjectUtils.isEmpty(id2)) {
            throw new IllegalArgumentException("用戶ID不能為空");
        }
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            log.info("事務(wù)B:開始更新id={}的用戶年齡", id2);
            userMapper.updateAgeById(id2, age2);
            Thread.sleep(1000);
            log.info("事務(wù)B:開始更新id={}的用戶年齡", id1);
            userMapper.updateAgeById(id1, age1);
            transactionManager.commit(status);
            log.info("事務(wù)B:執(zhí)行完成,事務(wù)提交");
        } catch (InterruptedException e) {
            transactionManager.rollback(status);
            Thread.currentThread().interrupt();
            log.error("事務(wù)B:線程中斷,事務(wù)回滾", e);
            throw new RuntimeException("更新失敗,線程中斷", e);
        } catch (Exception e) {
            transactionManager.rollback(status);
            log.error("事務(wù)B:更新異常,事務(wù)回滾", e);
            throw new RuntimeException("更新失敗", e);
        }
    }
}

控制器UserController

package com.jam.demo.controller;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import java.util.concurrent.CountDownLatch;
/**
 * 用戶控制器
 * @author ken
 */
@Slf4j
@RestController
@RequestMapping("/user")
@Tag(name = "用戶管理", description = "用戶相關(guān)操作接口")
public class UserController {
    @Resource
    private UserService userService;
    @PostMapping("/deadlock/simulate")
    @Operation(summary = "模擬交叉更新死鎖場景", description = "并發(fā)執(zhí)行兩個交叉更新的事務(wù),觸發(fā)死鎖")
    public String simulateDeadlock(
            @Parameter(description = "第一個用戶ID", example = "1") @RequestParam Long id1,
            @Parameter(description = "第二個用戶ID", example = "2") @RequestParam Long id2,
            @Parameter(description = "第一個用戶新年齡", example = "21") @RequestParam Integer age1,
            @Parameter(description = "第二個用戶新年齡", example = "26") @RequestParam Integer age2
    ) {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(() -> {
            try {
                userService.crossUpdateFirst(id1, id2, age1, age2 + 1);
            } catch (Exception e) {
                log.error("線程1執(zhí)行異常", e);
            } finally {
                countDownLatch.countDown();
            }
        }, "deadlock-thread-1").start();
        new Thread(() -> {
            try {
                userService.crossUpdateSecond(id1, id2, age1 + 1, age2);
            } catch (Exception e) {
                log.error("線程2執(zhí)行異常", e);
            } finally {
                countDownLatch.countDown();
            }
        }, "deadlock-thread-2").start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "模擬中斷";
        }
        return "死鎖模擬完成,請查看MySQL死鎖日志";
    }
}

application.yml配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: mysql-lock-demo
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    enabled: true
  api-docs:
    enabled: true
    path: /v3/api-docs
server:
  port: 8080

5.2 死鎖復(fù)現(xiàn)與排查

  1. 在MySQL中創(chuàng)建t_user表并插入初始數(shù)據(jù);
  2. 啟動Spring Boot項目,訪問http://localhost:8080/swagger-ui.html,調(diào)用/user/deadlock/simulate接口,傳入?yún)?shù)id1=1、id2=2、age1=21、age2=26;
  3. 接口執(zhí)行完成后,在MySQL中執(zhí)行show engine innodb status,查看死鎖日志;
  4. 通過日志解讀,定位到死鎖根因為兩個事務(wù)交叉更新id=1和id=2的記錄,形成循環(huán)等待。

5.3 死鎖修復(fù)方案

核心修復(fù)邏輯:統(tǒng)一資源訪問順序,打破循環(huán)等待條件 修改crossUpdateSecond方法,將更新順序調(diào)整為與crossUpdateFirst一致,均按照id從小到大的順序更新:

@Override
public void crossUpdateSecond(Long id1, Long id2, Integer age1, Integer age2) {
    if (ObjectUtils.isEmpty(id1) || ObjectUtils.isEmpty(id2)) {
        throw new IllegalArgumentException("用戶ID不能為空");
    }
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
    TransactionStatus status = transactionManager.getTransaction(def);
    try {
        // 統(tǒng)一更新順序:先更新id較小的記錄,再更新id較大的記錄
        Long firstId = Math.min(id1, id2);
        Long secondId = Math.max(id1, id2);
        Integer firstAge = firstId.equals(id1) ? age1 : age2;
        Integer secondAge = secondId.equals(id2) ? age2 : age1;
        log.info("事務(wù)B:開始更新id={}的用戶年齡", firstId);
        userMapper.updateAgeById(firstId, firstAge);
        Thread.sleep(1000);
        log.info("事務(wù)B:開始更新id={}的用戶年齡", secondId);
        userMapper.updateAgeById(secondId, secondAge);
        transactionManager.commit(status);
        log.info("事務(wù)B:執(zhí)行完成,事務(wù)提交");
    } catch (InterruptedException e) {
        transactionManager.rollback(status);
        Thread.currentThread().interrupt();
        log.error("事務(wù)B:線程中斷,事務(wù)回滾", e);
        throw new RuntimeException("更新失敗,線程中斷", e);
    } catch (Exception e) {
        transactionManager.rollback(status);
        log.error("事務(wù)B:更新異常,事務(wù)回滾", e);
        throw new RuntimeException("更新失敗", e);
    }
}

修改后,兩個事務(wù)均先更新id較小的記錄,再更新id較大的記錄,不會形成交叉等待,循環(huán)等待條件被打破,死鎖徹底解決。

六、鎖等待與死鎖的根治最佳實踐

6.1 SQL與索引優(yōu)化

  1. 所有更新、刪除語句必須通過explain檢查執(zhí)行計劃,確保走精準(zhǔn)索引,避免全表掃描導(dǎo)致的全表鎖;
  2. 盡量使用精準(zhǔn)匹配查詢,避免大范圍的范圍查詢,減少間隙鎖的生效范圍;
  3. 業(yè)務(wù)允許的情況下,將隔離級別調(diào)整為讀已提交(RC),RC級別下無間隙鎖,僅存在記錄鎖,可消除90%的間隙鎖導(dǎo)致的死鎖,同時半一致性讀可大幅減少鎖等待;
  4. 避免并發(fā)插入相同的唯一鍵值,減少唯一鍵沖突導(dǎo)致的S鎖申請。

6.2 業(yè)務(wù)代碼優(yōu)化

  1. 所有并發(fā)更新場景,必須統(tǒng)一資源的訪問順序,比如按主鍵從小到大、按業(yè)務(wù)編碼固定順序更新,打破循環(huán)等待條件;
  2. 嚴(yán)格控制事務(wù)粒度,事務(wù)內(nèi)的SQL數(shù)量盡量最少,避免在事務(wù)內(nèi)執(zhí)行RPC調(diào)用、IO操作等耗時邏輯,減少鎖的持有時間;
  3. 避免濫用select ... for update手動加鎖,若必須使用,確保查詢條件走精準(zhǔn)索引,且鎖定范圍最??;
  4. 優(yōu)先使用編程式事務(wù),避免聲明式事務(wù)傳播行為不當(dāng)導(dǎo)致的大事務(wù)。

6.3 數(shù)據(jù)庫配置優(yōu)化

  1. 確保innodb_deadlock_detect=ON,開啟死鎖檢測,默認(rèn)開啟;
  2. 開啟innodb_print_all_deadlocks=ON,將所有死鎖日志打印到MySQL錯誤日志,方便事后排查;
  3. 高并發(fā)場景下,適當(dāng)調(diào)小innodb_lock_wait_timeout,避免長時間的業(yè)務(wù)阻塞;
  4. 超高并發(fā)場景下,若死鎖檢測導(dǎo)致CPU占用過高,可通過分布式鎖、隊列削峰等方式控制并發(fā)度,避免直接關(guān)閉死鎖檢測。

總結(jié)

MySQL鎖等待與死鎖的本質(zhì),是InnoDB鎖機制下并發(fā)事務(wù)對資源的競爭形成的阻塞與循環(huán)等待。想要徹底解決這類問題,核心是先搞懂InnoDB鎖的底層邏輯與生效規(guī)則,再通過show engine innodb status精準(zhǔn)定位死鎖的循環(huán)等待鏈條,最終通過統(tǒng)一資源訪問順序、優(yōu)化索引、控制事務(wù)粒度等方式,打破死鎖的必要條件,從根因上解決問題。

以上就是MySQL鎖等待與死鎖根治的全攻略的詳細(xì)內(nèi)容,更多關(guān)于MySQL鎖等待與死鎖根治的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • MySQL 可擴展設(shè)計的基本原則

    MySQL 可擴展設(shè)計的基本原則

    可擴展設(shè)計是一個非常復(fù)雜的系統(tǒng)工程,所涉及的各個方面非常的廣泛,技術(shù)也較為復(fù)雜,可能還會帶來很多其他方面的問題。但不管我們?nèi)绾卧O(shè)計,不管遇到哪些問題,有些原則我們還是必須確保的。
    2021-05-05
  • MySQL新手入門指南--快速參考

    MySQL新手入門指南--快速參考

    MySQL新手入門指南--快速參考...
    2006-11-11
  • MySQL安裝與配置:手工配置MySQL(windows環(huán)境)過程

    MySQL安裝與配置:手工配置MySQL(windows環(huán)境)過程

    這篇文章主要介紹了MySQL安裝與配置:手工配置MySQL(windows環(huán)境)過程,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • MySQL添加索引的優(yōu)化與實踐

    MySQL添加索引的優(yōu)化與實踐

    在數(shù)據(jù)庫中,索引是提升查詢性能的核心工具之一,MySQL 提供了豐富的索引選項,使得我們能夠根據(jù)不同的查詢需求和數(shù)據(jù)量來設(shè)計和優(yōu)化索引,本文將深入探討 MySQL 中添加索引的一些常見問題、最佳實踐以及如何在大數(shù)據(jù)量的表上高效添加索引,需要的朋友可以參考下
    2024-11-11
  • MySQL部署后連接被拒絕問題的排查與解決方法詳細(xì)指南

    MySQL部署后連接被拒絕問題的排查與解決方法詳細(xì)指南

    MySQL是一個開源的、關(guān)系型數(shù)據(jù)庫管理系統(tǒng),在開發(fā)過程中被廣泛使用,有時候我們可能會遇到MySQL連接不上本地服務(wù)器的問題,這篇文章主要介紹了MySQL部署后連接被拒絕問題的排查與解決方法,需要的朋友可以參考下
    2025-11-11
  • MySQL基于DOS命令行登錄操作實例(圖文說明)

    MySQL基于DOS命令行登錄操作實例(圖文說明)

    這篇文章主要介紹了MySQL基于DOS命令行登錄操作,以圖文形式結(jié)合實例說明了MySQL登錄命令的基本用法,非常簡單易懂需要的朋友可以參考下
    2016-01-01
  • mysql刪除語句超詳細(xì)匯總

    mysql刪除語句超詳細(xì)匯總

    這篇文章主要給大家介紹了關(guān)于mysql刪除語句超詳細(xì)匯總的相關(guān)資料,SQL是用于訪問和處理數(shù)據(jù)庫的標(biāo)準(zhǔn)的計算機語言,簡稱結(jié)構(gòu)化查詢語言,SQL中的刪除語句有多種方法,這里總結(jié)下,需要的朋友可以參考下
    2023-08-08
  • 最新評論

    云安县| 宾川县| 楚雄市| 阳信县| 青铜峡市| 金沙县| 巴林左旗| 任丘市| 海安县| 沂水县| 玉林市| 咸丰县| 樟树市| 泊头市| 清原| 屏边| 吉木乃县| 柳河县| 红桥区| 英超| 比如县| 都匀市| 武鸣县| 沈阳市| 前郭尔| 德州市| 宜兰县| 吉首市| 泊头市| 龙海市| 佛学| 临湘市| 乌兰察布市| 南川市| 瑞金市| 奎屯市| 祁东县| 南郑县| 达拉特旗| 阿勒泰市| 蒙山县|