MySQL鎖等待與死鎖根治的全攻略
線上業(yè)務突然拋出Lock wait timeout exceeded異常、接口超時、甚至事務被強制回滾,90%以上的場景都源于MySQL鎖等待與死鎖問題。很多開發(fā)者面對問題時無從下手,要么只會重啟服務,要么找不到根因?qū)е聠栴}反復出現(xiàn)。
一、InnoDB鎖體系核心前置認知
想要排查鎖問題,必須先搞懂InnoDB鎖的底層邏輯,90%的鎖問題排查失敗,都源于對鎖類型、兼容性、生效規(guī)則的認知錯誤。
1.1 鎖的核心維度劃分

1.1.1 按兼容性劃分的基礎鎖類型
這是鎖機制的核心基礎,兼容性矩陣決定了鎖是否會發(fā)生阻塞,所有規(guī)則100%遵循MySQL 8.0官方規(guī)范:
| 鎖類型 | 共享鎖(S) | 排他鎖(X) | 意向共享鎖(IS) | 意向排他鎖(IX) |
|---|---|---|---|---|
| 共享鎖(S) | 兼容 | 互斥 | 兼容 | 互斥 |
| 排他鎖(X) | 互斥 | 互斥 | 互斥 | 互斥 |
| 意向共享鎖(IS) | 兼容 | 互斥 | 兼容 | 兼容 |
| 意向排他鎖(IX) | 互斥 | 互斥 | 兼容 | 兼容 |
- 共享鎖(S鎖) :讀鎖,多個事務可同時持有同一行的S鎖,用于
select ... lock in share mode,持有S鎖的事務只能讀不能修改該行。 - 排他鎖(X鎖) :寫鎖,一個事務持有某行的X鎖后,其他事務不能持有該行的任何鎖,
UPDATE/DELETE/INSERT語句會自動加X鎖,select ... for update手動加X鎖。 - 意向鎖(IS/IX) :表級鎖,用于快速判斷表內(nèi)是否有行鎖,避免全表掃描檢查行鎖,事務加行鎖前必須先加對應的意向鎖,意向鎖之間互相兼容,僅與表級的S/X鎖互斥。
1.1.2 行級鎖的細分類型(RR隔離級別,MySQL 8.0默認)
這是死鎖問題的核心重災區(qū),必須精準理解每一種鎖的生效規(guī)則:
- 記錄鎖(Record Lock) :僅鎖住索引中的某一行具體記錄,只在
唯一索引+精準匹配的場景下生效,比如where id=1(id為主鍵),僅鎖住id=1的行,對其他行完全無影響。 - 間隙鎖(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ū)間的操作,但其他事務可以同時持有該間隙的間隙鎖。 - 臨鍵鎖(Next-Key Lock) :InnoDB RR級別下默認的行鎖算法,由「記錄鎖+該記錄左邊的間隙鎖」組成,左開右閉區(qū)間,比如上述id的臨鍵鎖區(qū)間為
(-∞,1]、(1,3]、(3,5]、(5,+∞]。僅當查詢條件為唯一索引+精準匹配時,臨鍵鎖會退化為記錄鎖,其他場景均為臨鍵鎖。 - 插入意向鎖(Insert Intention Lock) :一種特殊的間隙鎖,
INSERT語句插入前會先申請該鎖,它與間隙鎖互斥,與其他插入意向鎖兼容,是間隙鎖場景下死鎖的核心誘因。
1.2 鎖等待與死鎖的核心區(qū)別
很多開發(fā)者會將兩者混為一談,實際上二者的觸發(fā)邏輯、處理機制完全不同:

二、鎖等待的根因拆解與全鏈路排查方法 論
2.1 鎖等待的高頻根因
- 長事務未提交占用行鎖:事務執(zhí)行完更新語句后未及時提交/回滾,長期持有行鎖,導致后續(xù)所有操作該行的事務全部阻塞,是線上最常見的鎖等待誘因。
- 索引失效導致行鎖升級為全表鎖:更新/刪除語句的查詢條件沒有索引,或索引失效,InnoDB無法精準定位行,會走全表掃描并給所有行加臨鍵鎖,相當于鎖全表,任何更新操作都會被阻塞。
- 熱點行并發(fā)更新:秒殺、庫存扣減等場景,大量并發(fā)請求同時更新同一行記錄,導致后續(xù)事務排隊等待鎖釋放,出現(xiàn)大面積鎖等待超時。
- 手動加鎖范圍過大:濫用
select ... for update,查詢條件范圍過大或無索引,導致鎖住大量無關行,阻塞其他業(yè)務操作。
2.2 鎖等待的標準化排查步驟
MySQL 8.0.13之后,鎖信息從information_schema.INNODB_LOCKS遷移至performance_schema.data_locks,以下SQL均適配MySQL 8.0最新規(guī)范。
步驟1:定位阻塞的線程與SQL
執(zhí)行以下命令,找到處于鎖等待狀態(tài)的線程:
show processlist;
重點關注State列值為Waiting for row lock/Waiting for table metadata lock的線程,記錄Id(MySQL線程ID)、Time(阻塞時長)、Info(正在執(zhí)行的SQL)。
步驟2:查詢鎖等待的關聯(lián)關系
執(zhí)行以下SQL,直接定位「等待鎖的事務」與「持有鎖的事務」的對應關系:
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:查看鎖的詳細信息
執(zhí)行以下SQL,查看當前數(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:根因定位與應急處理
- 若阻塞源是未提交的長事務,可通過
kill 阻塞線程ID臨時釋放鎖,恢復業(yè)務; - 若為索引失效導致的全表鎖,立即給查詢條件添加合適的索引,確保更新語句走精準索引;
- 若為熱點行更新,優(yōu)化業(yè)務邏輯,采用分布式鎖、隊列削峰等方式控制并發(fā)度。
三、死鎖的形成條件與高頻場景復現(xiàn)
3.1 死鎖形成的4個必要條件
死鎖的觸發(fā)必須同時滿足以下4個條件,只要打破其中任意一個,就能徹底避免死鎖:
- 互斥條件:鎖只能被一個事務持有,其他事務無法同時持有同一把鎖;
- 占有且等待:事務已持有至少一把鎖,又申請新的鎖,且新鎖被其他事務持有時,不釋放已持有的鎖;
- 不可搶占:事務已持有的鎖,只能由自身提交/回滾釋放,無法被其他事務強制搶占;
- 循環(huán)等待:多個事務形成頭尾相接的等待閉環(huán),互相等待對方持有的鎖。
3.2 線上高頻死鎖場景與完整復現(xiàn)
以下所有場景均基于MySQL 8.0默認RR隔離級別,SQL可直接執(zhí)行復現(xiàn)。
場景1:交叉更新行導致的死鎖(最經(jīng)典、最高發(fā))
表結構與初始化數(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);
死鎖復現(xiàn)步驟(按時間順序執(zhí)行)
| 時間 | 事務A | 事務B |
|---|---|---|
| T1 | BEGIN; | BEGIN; |
| T2 | UPDATE t_user SET age=21 WHERE id=1; | UPDATE t_user SET age=26 WHERE id=2; |
| T3 | UPDATE t_user SET age=22 WHERE id=2; | |
| T4 | 阻塞,等待id=2的行X鎖 | UPDATE t_user SET age=27 WHERE id=1; |
| T5 | 死鎖觸發(fā),事務被回滾 |
根因分析:事務A持有id=1的X鎖,申請id=2的X鎖;事務B持有id=2的X鎖,申請id=1的X鎖,形成循環(huán)等待閉環(huán),4個死鎖條件全部滿足,觸發(fā)死鎖。
場景2:間隙鎖+插入意向鎖導致的死鎖(90%線上隱藏死鎖的誘因)
表結構復用上述t_user表,當前數(shù)據(jù)id為1、2,存在間隙(2,+∞)
死鎖復現(xiàn)步驟(按時間順序執(zhí)行)
| 時間 | 事務A | 事務B |
|---|---|---|
| T1 | BEGIN; | BEGIN; |
| T2 | SELECT * FROM t_user WHERE id=3 FOR UPDATE; | SELECT * FROM t_user WHERE id=4 FOR UPDATE; |
| T3 | 持有(2,+∞)的間隙鎖 | 持有(2,+∞)的間隙鎖(間隙鎖之間兼容,無阻塞) |
| T4 | INSERT INTO t_user (id, user_name, age) VALUES (3, '王五', 30); | |
| T5 | 阻塞,申請插入意向鎖,被事務B的間隙鎖阻塞 | INSERT INTO t_user (id, user_name, age) VALUES (4, '趙六', 35); |
| T6 | 阻塞,申請插入意向鎖,被事務A的間隙鎖阻塞,死鎖觸發(fā) |
根因分析:兩個事務同時持有同一間隙的間隙鎖,又同時申請插入意向鎖,而插入意向鎖與間隙鎖互斥,形成循環(huán)等待,觸發(fā)死鎖。該場景是線上最高發(fā)的隱藏死鎖,很多開發(fā)者因不了解間隙鎖的兼容規(guī)則,無法定位根因。
場景3:唯一鍵沖突導致的死鎖
表結構復用上述t_user表,user_name為唯一索引
死鎖復現(xiàn)步驟(按時間順序執(zhí)行)
| 時間 | 事務A | 事務B | 事務C |
|---|---|---|---|
| T1 | BEGIN; | BEGIN; | BEGIN; |
| T2 | INSERT INTO t_user (user_name, age) VALUES ('test', 18); | ||
| T3 | INSERT INTO t_user (user_name, age) VALUES ('test', 18); | INSERT INTO t_user (user_name, age) VALUES ('test', 18); | |
| T4 | 唯一鍵沖突,阻塞,申請S鎖 | 唯一鍵沖突,阻塞,申請S鎖 | |
| T5 | ROLLBACK; | ||
| T6 | 事務A釋放鎖,B和C同時拿到S鎖 | 持有S鎖,申請X鎖完成插入 | 持有S鎖,申請X鎖完成插入 |
| T7 | 阻塞,等待C的S鎖釋放 | 阻塞,等待B的S鎖釋放,死鎖觸發(fā) |
根因分析:唯一鍵沖突時,InnoDB不會直接報錯,而是會給沖突的記錄申請S鎖;事務A回滾后,B和C同時持有S鎖,又同時申請X鎖,X鎖與S鎖互斥,形成循環(huán)等待,觸發(fā)死鎖。
四、show engine innodb status 死鎖日志完整解讀與精準定位
show engine innodb status是MySQL排查死鎖的終極工具,它會記錄最近一次死鎖的完整信息,包含參與事務的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 死鎖日志逐行精準解讀
1. 死鎖頭部信息
2026-04-08 10:00:00 0x7f8a1b2c3700
- 第一部分:死鎖發(fā)生的精確時間;
- 第二部分:觸發(fā)死鎖的InnoDB OS線程ID,可用于關聯(lián)MySQL錯誤日志。
2. 第一個參與死鎖的事務(事務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:事務的唯一ID,可用于關聯(lián)事務詳情表;ACTIVE 10 sec:事務已活躍10秒,鎖持有時間過長是死鎖的重要誘因;LOCK WAIT:當前事務處于鎖等待狀態(tài);MySQL thread id 12:MySQL線程ID,可通過kill 12終止該線程;- 最后一行:事務當前正在執(zhí)行的、被阻塞的SQL語句,可直接定位到業(yè)務代碼位置。
3. 事務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:鎖加在主鍵索引上,可直接定位到鎖對應的索引;table test.t_user:鎖對應的庫名和表名;lock_mode X locks rec but not gap waiting:鎖模式為排他鎖(X),僅鎖記錄不鎖間隙,當前處于等待狀態(tài);0: len 8; hex 8000000000000002:主鍵索引的字段值,十六進制轉換為十進制為2,即事務1正在等待id=2的主鍵記錄的X鎖。
4. 第二個參與死鎖的事務(事務2)
結構與事務1完全一致,核心信息為:事務ID為42108,MySQL線程ID為13,正在執(zhí)行的SQL為UPDATE t_user SET age=27 WHERE id=1。
5. 事務2持有的鎖信息(核心關聯(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 ;;
該部分明確顯示:事務2持有test.t_user表主鍵索引上id=2的記錄的X鎖,正好是事務1正在等待的鎖。
6. 事務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 ;;
該部分明確顯示:事務2正在等待test.t_user表主鍵索引上id=1的記錄的X鎖,而該鎖正好被事務1持有。
7. 死鎖處理結果
*** WE ROLL BACK TRANSACTION (1)
InnoDB死鎖檢測線程會計算兩個事務的回滾代價,選擇代價最小的事務進行回滾,此處回滾了僅持有1個行鎖的事務1。
4.3 鎖模式關鍵字段解讀
日志中鎖模式的關鍵字段直接決定了死鎖的類型,必須精準理解:
| 字段 | 含義 |
|---|---|
| lock_mode X | 排他鎖 |
| lock_mode S | 共享鎖 |
| locks rec but not gap | 僅記錄鎖,無間隙鎖 |
| gap | 僅間隙鎖,不鎖記錄 |
| locks gap before rec | 臨鍵鎖(記錄鎖+左邊間隙鎖) |
| insert intention | 插入意向鎖 |
| waiting | 正在等待該鎖 |
五、死鎖全鏈路實戰(zhàn)排查與修復
以下通過Java業(yè)務代碼復現(xiàn)死鎖,再通過上述方法定位根因,最終給出修復方案,形成完整的排查閉環(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);
}服務實現(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;
/**
* 用戶服務實現(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("事務A:開始更新id={}的用戶年齡", id1);
userMapper.updateAgeById(id1, age1);
Thread.sleep(1000);
log.info("事務A:開始更新id={}的用戶年齡", id2);
userMapper.updateAgeById(id2, age2);
transactionManager.commit(status);
log.info("事務A:執(zhí)行完成,事務提交");
} catch (InterruptedException e) {
transactionManager.rollback(status);
Thread.currentThread().interrupt();
log.error("事務A:線程中斷,事務回滾", e);
throw new RuntimeException("更新失敗,線程中斷", e);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("事務A:更新異常,事務回滾", 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("事務B:開始更新id={}的用戶年齡", id2);
userMapper.updateAgeById(id2, age2);
Thread.sleep(1000);
log.info("事務B:開始更新id={}的用戶年齡", id1);
userMapper.updateAgeById(id1, age1);
transactionManager.commit(status);
log.info("事務B:執(zhí)行完成,事務提交");
} catch (InterruptedException e) {
transactionManager.rollback(status);
Thread.currentThread().interrupt();
log.error("事務B:線程中斷,事務回滾", e);
throw new RuntimeException("更新失敗,線程中斷", e);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("事務B:更新異常,事務回滾", 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 = "用戶相關操作接口")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/deadlock/simulate")
@Operation(summary = "模擬交叉更新死鎖場景", description = "并發(fā)執(zhí)行兩個交叉更新的事務,觸發(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: 80805.2 死鎖復現(xiàn)與排查
- 在MySQL中創(chuàng)建
t_user表并插入初始數(shù)據(jù); - 啟動Spring Boot項目,訪問
http://localhost:8080/swagger-ui.html,調(diào)用/user/deadlock/simulate接口,傳入?yún)?shù)id1=1、id2=2、age1=21、age2=26; - 接口執(zhí)行完成后,在MySQL中執(zhí)行
show engine innodb status,查看死鎖日志; - 通過日志解讀,定位到死鎖根因為兩個事務交叉更新id=1和id=2的記錄,形成循環(huán)等待。
5.3 死鎖修復方案
核心修復邏輯:統(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("事務B:開始更新id={}的用戶年齡", firstId);
userMapper.updateAgeById(firstId, firstAge);
Thread.sleep(1000);
log.info("事務B:開始更新id={}的用戶年齡", secondId);
userMapper.updateAgeById(secondId, secondAge);
transactionManager.commit(status);
log.info("事務B:執(zhí)行完成,事務提交");
} catch (InterruptedException e) {
transactionManager.rollback(status);
Thread.currentThread().interrupt();
log.error("事務B:線程中斷,事務回滾", e);
throw new RuntimeException("更新失敗,線程中斷", e);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("事務B:更新異常,事務回滾", e);
throw new RuntimeException("更新失敗", e);
}
}修改后,兩個事務均先更新id較小的記錄,再更新id較大的記錄,不會形成交叉等待,循環(huán)等待條件被打破,死鎖徹底解決。
六、鎖等待與死鎖的根治最佳實踐
6.1 SQL與索引優(yōu)化
- 所有更新、刪除語句必須通過
explain檢查執(zhí)行計劃,確保走精準索引,避免全表掃描導致的全表鎖; - 盡量使用精準匹配查詢,避免大范圍的范圍查詢,減少間隙鎖的生效范圍;
- 業(yè)務允許的情況下,將隔離級別調(diào)整為讀已提交(RC),RC級別下無間隙鎖,僅存在記錄鎖,可消除90%的間隙鎖導致的死鎖,同時半一致性讀可大幅減少鎖等待;
- 避免并發(fā)插入相同的唯一鍵值,減少唯一鍵沖突導致的S鎖申請。
6.2 業(yè)務代碼優(yōu)化
- 所有并發(fā)更新場景,必須統(tǒng)一資源的訪問順序,比如按主鍵從小到大、按業(yè)務編碼固定順序更新,打破循環(huán)等待條件;
- 嚴格控制事務粒度,事務內(nèi)的SQL數(shù)量盡量最少,避免在事務內(nèi)執(zhí)行RPC調(diào)用、IO操作等耗時邏輯,減少鎖的持有時間;
- 避免濫用
select ... for update手動加鎖,若必須使用,確保查詢條件走精準索引,且鎖定范圍最??; - 優(yōu)先使用編程式事務,避免聲明式事務傳播行為不當導致的大事務。
6.3 數(shù)據(jù)庫配置優(yōu)化
- 確保
innodb_deadlock_detect=ON,開啟死鎖檢測,默認開啟; - 開啟
innodb_print_all_deadlocks=ON,將所有死鎖日志打印到MySQL錯誤日志,方便事后排查; - 高并發(fā)場景下,適當調(diào)小
innodb_lock_wait_timeout,避免長時間的業(yè)務阻塞; - 超高并發(fā)場景下,若死鎖檢測導致CPU占用過高,可通過分布式鎖、隊列削峰等方式控制并發(fā)度,避免直接關閉死鎖檢測。
總結
MySQL鎖等待與死鎖的本質(zhì),是InnoDB鎖機制下并發(fā)事務對資源的競爭形成的阻塞與循環(huán)等待。想要徹底解決這類問題,核心是先搞懂InnoDB鎖的底層邏輯與生效規(guī)則,再通過show engine innodb status精準定位死鎖的循環(huán)等待鏈條,最終通過統(tǒng)一資源訪問順序、優(yōu)化索引、控制事務粒度等方式,打破死鎖的必要條件,從根因上解決問題。
以上就是MySQL鎖等待與死鎖根治的全攻略的詳細內(nèi)容,更多關于MySQL鎖等待與死鎖根治的資料請關注腳本之家其它相關文章!
相關文章
MySQL安裝與配置:手工配置MySQL(windows環(huán)境)過程

