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

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

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

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

一、InnoDB鎖體系核心前置認知

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

1.1 鎖的核心維度劃分

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ī)則:

  1. 記錄鎖(Record Lock) :僅鎖住索引中的某一行具體記錄,只在唯一索引+精準匹配的場景下生效,比如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ū)間的操作,但其他事務可以同時持有該間隙的間隙鎖。
  3. 臨鍵鎖(Next-Key Lock) :InnoDB RR級別下默認的行鎖算法,由「記錄鎖+該記錄左邊的間隙鎖」組成,左開右閉區(qū)間,比如上述id的臨鍵鎖區(qū)間為(-∞,1]、(1,3]、(3,5]、(5,+∞]。僅當查詢條件為唯一索引+精準匹配時,臨鍵鎖會退化為記錄鎖,其他場景均為臨鍵鎖。
  4. 插入意向鎖(Insert Intention Lock) :一種特殊的間隙鎖,INSERT語句插入前會先申請該鎖,它與間隙鎖互斥,與其他插入意向鎖兼容,是間隙鎖場景下死鎖的核心誘因。

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

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

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

2.1 鎖等待的高頻根因

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

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

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

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

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

  1. 互斥條件:鎖只能被一個事務持有,其他事務無法同時持有同一把鎖;
  2. 占有且等待:事務已持有至少一把鎖,又申請新的鎖,且新鎖被其他事務持有時,不釋放已持有的鎖;
  3. 不可搶占:事務已持有的鎖,只能由自身提交/回滾釋放,無法被其他事務強制搶占;
  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
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ā),事務被回滾

根因分析:事務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
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阻塞,申請插入意向鎖,被事務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
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事務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: 8080

5.2 死鎖復現(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. 通過日志解讀,定位到死鎖根因為兩個事務交叉更新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)化

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

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

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

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

  1. 確保innodb_deadlock_detect=ON,開啟死鎖檢測,默認開啟;
  2. 開啟innodb_print_all_deadlocks=ON,將所有死鎖日志打印到MySQL錯誤日志,方便事后排查;
  3. 高并發(fā)場景下,適當調(diào)小innodb_lock_wait_timeout,避免長時間的業(yè)務阻塞;
  4. 超高并發(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 可擴展設計的基本原則

    可擴展設計是一個非常復雜的系統(tǒng)工程,所涉及的各個方面非常的廣泛,技術也較為復雜,可能還會帶來很多其他方面的問題。但不管我們?nèi)绾卧O計,不管遇到哪些問題,有些原則我們還是必須確保的。
    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ù)量來設計和優(yōu)化索引,本文將深入探討 MySQL 中添加索引的一些常見問題、最佳實踐以及如何在大數(shù)據(jù)量的表上高效添加索引,需要的朋友可以參考下
    2024-11-11
  • MySQL部署后連接被拒絕問題的排查與解決方法詳細指南

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

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

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

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

    mysql刪除語句超詳細匯總

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

    勃利县| 松潘县| 辽宁省| 元氏县| 理塘县| 鸡西市| 隆子县| 巴东县| 光泽县| 贡嘎县| 泰州市| 邹城市| 应城市| 东山县| 札达县| 兴安县| 曲阜市| 巨鹿县| 栾城县| 滕州市| 洮南市| 安西县| 梅州市| 宁德市| 嵩明县| 古浪县| 南靖县| 民权县| 洱源县| 辛集市| 桂林市| 甘德县| 海原县| 金乡县| 高雄县| 同江市| 郓城县| 庆阳市| 木兰县| 前郭尔| 忻城县|