MySQL 锁等待与死锁根治全攻略:从底层原理到 innodb status 精准定位实战

简介: 本文深入解析MySQL InnoDB锁机制,涵盖锁类型、兼容性、行级锁(记录锁/间隙锁/临键锁/插入意向锁)及死锁4大条件;提供锁等待与死锁的全链路排查方法(含performance_schema SQL)、日志精准解读,并结合Java实战复现与修复,助开发者根治超时、回滚等线上顽疾。

线上业务突然抛出Lock wait timeout exceeded异常、接口超时、甚至事务被强制回滚,90%以上的场景都源于MySQL锁等待与死锁问题。很多开发者面对问题时无从下手,要么只会重启服务,要么找不到根因导致问题反复出现。

一、InnoDB锁体系核心前置认知

想要排查锁问题,必须先搞懂InnoDB锁的底层逻辑,90%的锁问题排查失败,都源于对锁类型、兼容性、生效规则的认知错误。

1.1 锁的核心维度划分

1.1.1 按兼容性划分的基础锁类型

这是锁机制的核心基础,兼容性矩阵决定了锁是否会发生阻塞,所有规则100%遵循MySQL 8.0官方规范

锁类型 共享锁(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) :表级锁,用于快速判断表内是否有行锁,避免全表扫描检查行锁,事务加行锁前必须先加对应的意向锁,意向锁之间互相兼容,仅与表级的S/X锁互斥。

1.1.2 行级锁的细分类型(RR隔离级别,MySQL 8.0默认)

这是死锁问题的核心重灾区,必须精准理解每一种锁的生效规则:

  1. 记录锁(Record Lock) :仅锁住索引中的某一行具体记录,只在唯一索引+精准匹配的场景下生效,比如where id=1(id为主键),仅锁住id=1的行,对其他行完全无影响。
  2. 间隙锁(Gap Lock) :锁住索引记录之间的间隙,仅用于防止幻读,核心规则:间隙锁之间完全兼容,仅与插入意向锁互斥。比如表中id有1、3、5,间隙分为(-∞,1)、(1,3)、(3,5)、(5,+∞),执行select * from t where id=2 for update会锁住(1,3)间隙,禁止任何插入该区间的操作,但其他事务可以同时持有该间隙的间隙锁。
  3. 临键锁(Next-Key Lock) :InnoDB RR级别下默认的行锁算法,由「记录锁+该记录左边的间隙锁」组成,左开右闭区间,比如上述id的临键锁区间为(-∞,1]、(1,3]、(3,5]、(5,+∞]。仅当查询条件为唯一索引+精准匹配时,临键锁会退化为记录锁,其他场景均为临键锁。
  4. 插入意向锁(Insert Intention Lock) :一种特殊的间隙锁,INSERT语句插入前会先申请该锁,它与间隙锁互斥,与其他插入意向锁兼容,是间隙锁场景下死锁的核心诱因。

1.2 锁等待与死锁的核心区别

很多开发者会将两者混为一谈,实际上二者的触发逻辑、处理机制完全不同:

二、锁等待的根因拆解与全链路排查方法论

2.1 锁等待的高频根因

  1. 长事务未提交占用行锁:事务执行完更新语句后未及时提交/回滚,长期持有行锁,导致后续所有操作该行的事务全部阻塞,是线上最常见的锁等待诱因。
  2. 索引失效导致行锁升级为全表锁:更新/删除语句的查询条件没有索引,或索引失效,InnoDB无法精准定位行,会走全表扫描并给所有行加临键锁,相当于锁全表,任何更新操作都会被阻塞。
  3. 热点行并发更新:秒杀、库存扣减等场景,大量并发请求同时更新同一行记录,导致后续事务排队等待锁释放,出现大面积锁等待超时。
  4. 手动加锁范围过大:滥用select ... for update,查询条件范围过大或无索引,导致锁住大量无关行,阻塞其他业务操作。

2.2 锁等待的标准化排查步骤

MySQL 8.0.13之后,锁信息从information_schema.INNODB_LOCKS迁移至performance_schema.data_locks,以下SQL均适配MySQL 8.0最新规范。

步骤1:定位阻塞的线程与SQL

执行以下命令,找到处于锁等待状态的线程:

show processlist;

重点关注State列值为Waiting for row lock/Waiting for table metadata lock的线程,记录Id(MySQL线程ID)、Time(阻塞时长)、Info(正在执行的SQL)。

步骤2:查询锁等待的关联关系

执行以下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:查看锁的详细信息

执行以下SQL,查看当前数据库中所有持有的锁详情,包括锁类型、锁所在的表、索引、具体记录:

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临时释放锁,恢复业务;
  2. 若为索引失效导致的全表锁,立即给查询条件添加合适的索引,确保更新语句走精准索引;
  3. 若为热点行更新,优化业务逻辑,采用分布式锁、队列削峰等方式控制并发度。

三、死锁的形成条件与高频场景复现

3.1 死锁形成的4个必要条件

死锁的触发必须同时满足以下4个条件,只要打破其中任意一个,就能彻底避免死锁

  1. 互斥条件:锁只能被一个事务持有,其他事务无法同时持有同一把锁;
  2. 占有且等待:事务已持有至少一把锁,又申请新的锁,且新锁被其他事务持有时,不释放已持有的锁;
  3. 不可抢占:事务已持有的锁,只能由自身提交/回滚释放,无法被其他事务强制抢占;
  4. 循环等待:多个事务形成头尾相接的等待闭环,互相等待对方持有的锁。

3.2 线上高频死锁场景与完整复现

以下所有场景均基于MySQL 8.0默认RR隔离级别,SQL可直接执行复现。

场景1:交叉更新行导致的死锁(最经典、最高发)

表结构与初始化数据

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);

死锁复现步骤(按时间顺序执行)

时间 事务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 死锁触发,事务被回滚

根因分析:事务A持有id=1的X锁,申请id=2的X锁;事务B持有id=2的X锁,申请id=1的X锁,形成循环等待闭环,4个死锁条件全部满足,触发死锁。

场景2:间隙锁+插入意向锁导致的死锁(90%线上隐藏死锁的诱因)

表结构复用上述t_user表,当前数据id为1、2,存在间隙(2,+∞)

死锁复现步骤(按时间顺序执行)

时间 事务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的间隙锁阻塞,死锁触发

根因分析:两个事务同时持有同一间隙的间隙锁,又同时申请插入意向锁,而插入意向锁与间隙锁互斥,形成循环等待,触发死锁。该场景是线上最高发的隐藏死锁,很多开发者因不了解间隙锁的兼容规则,无法定位根因。

场景3:唯一键冲突导致的死锁

表结构复用上述t_user表,user_name为唯一索引

死锁复现步骤(按时间顺序执行)

时间 事务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锁释放,死锁触发

根因分析:唯一键冲突时,InnoDB不会直接报错,而是会给冲突的记录申请S锁;事务A回滚后,B和C同时持有S锁,又同时申请X锁,X锁与S锁互斥,形成循环等待,触发死锁。

四、show engine innodb status 死锁日志完整解读与精准定位

show engine innodb status是MySQL排查死锁的终极工具,它会记录最近一次死锁的完整信息,包含参与事务的SQL、持有的锁、等待的锁、循环等待链条等核心信息,只要读懂这份日志,就能100%定位死锁源头。

4.1 死锁日志完整示例

以下是上述场景1交叉更新触发的完整死锁日志,来自MySQL 8.0环境:

------------------------
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

  • 第一部分:死锁发生的精确时间;
  • 第二部分:触发死锁的InnoDB OS线程ID,可用于关联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,可用于关联事务详情表;
  • ACTIVE 10 sec:事务已活跃10秒,锁持有时间过长是死锁的重要诱因;
  • LOCK WAIT:当前事务处于锁等待状态;
  • MySQL thread id 12:MySQL线程ID,可通过kill 12终止该线程;
  • 最后一行:事务当前正在执行的、被阻塞的SQL语句,可直接定位到业务代码位置。

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),仅锁记录不锁间隙,当前处于等待状态;
  • 0: len 8; hex 8000000000000002:主键索引的字段值,十六进制转换为十进制为2,即事务1正在等待id=2的主键记录的X锁

4. 第二个参与死锁的事务(事务2)

结构与事务1完全一致,核心信息为:事务ID为42108,MySQL线程ID为13,正在执行的SQL为UPDATE t_user SET age=27 WHERE id=1

5. 事务2持有的锁信息(核心关联点)

*** (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 正在等待该锁

五、死锁全链路实战排查与修复

以下通过Java业务代码复现死锁,再通过上述方法定位根因,最终给出修复方案,形成完整的排查闭环。

5.1 项目环境与核心代码

项目基于JDK 17、Spring Boot 3.2.5、MyBatis-Plus 3.5.7开发。

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锁与死锁实战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> {

   /**
    * 根据ID更新用户年龄
    * @param id 用户ID
    * @param age 新年龄
    * @return 影响行数
    */

   @Update("UPDATE t_user SET age = #{age} WHERE id = #{id}")
   int updateAgeById(@Param("id") Long id, @Param("age") Integer age);
}

服务实现类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;

/**
* 用户服务实现类
* @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:执行完成,事务提交");
       } 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:执行完成,事务提交");
       } 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 = "并发执行两个交叉更新的事务,触发死锁")
   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执行异常", e);
           } finally {
               countDownLatch.countDown();
           }
       }, "deadlock-thread-1").start();

       new Thread(() -> {
           try {
               userService.crossUpdateSecond(id1, id2, age1 + 1, age2);
           } catch (Exception e) {
               log.error("线程2执行异常", 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 死锁复现与排查

  1. 在MySQL中创建t_user表并插入初始数据;
  2. 启动Spring Boot项目,访问http://localhost:8080/swagger-ui.html,调用/user/deadlock/simulate接口,传入参数id1=1、id2=2、age1=21、age2=26
  3. 接口执行完成后,在MySQL中执行show engine innodb status,查看死锁日志;
  4. 通过日志解读,定位到死锁根因为两个事务交叉更新id=1和id=2的记录,形成循环等待。

5.3 死锁修复方案

核心修复逻辑:统一资源访问顺序,打破循环等待条件修改crossUpdateSecond方法,将更新顺序调整为与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 {
       // 统一更新顺序:先更新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:执行完成,事务提交");
   } 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较大的记录,不会形成交叉等待,循环等待条件被打破,死锁彻底解决。

六、锁等待与死锁的根治最佳实践

6.1 SQL与索引优化

  1. 所有更新、删除语句必须通过explain检查执行计划,确保走精准索引,避免全表扫描导致的全表锁;
  2. 尽量使用精准匹配查询,避免大范围的范围查询,减少间隙锁的生效范围;
  3. 业务允许的情况下,将隔离级别调整为**读已提交(RC)**,RC级别下无间隙锁,仅存在记录锁,可消除90%的间隙锁导致的死锁,同时半一致性读可大幅减少锁等待;
  4. 避免并发插入相同的唯一键值,减少唯一键冲突导致的S锁申请。

6.2 业务代码优化

  1. 所有并发更新场景,必须统一资源的访问顺序,比如按主键从小到大、按业务编码固定顺序更新,打破循环等待条件;
  2. 严格控制事务粒度,事务内的SQL数量尽量最少,避免在事务内执行RPC调用、IO操作等耗时逻辑,减少锁的持有时间;
  3. 避免滥用select ... for update手动加锁,若必须使用,确保查询条件走精准索引,且锁定范围最小;
  4. 优先使用编程式事务,避免声明式事务传播行为不当导致的大事务。

6.3 数据库配置优化

  1. 确保innodb_deadlock_detect=ON,开启死锁检测,默认开启;
  2. 开启innodb_print_all_deadlocks=ON,将所有死锁日志打印到MySQL错误日志,方便事后排查;
  3. 高并发场景下,适当调小innodb_lock_wait_timeout,避免长时间的业务阻塞;
  4. 超高并发场景下,若死锁检测导致CPU占用过高,可通过分布式锁、队列削峰等方式控制并发度,避免直接关闭死锁检测。

总结

MySQL锁等待与死锁的本质,是InnoDB锁机制下并发事务对资源的竞争形成的阻塞与循环等待。想要彻底解决这类问题,核心是先搞懂InnoDB锁的底层逻辑与生效规则,再通过show engine innodb status精准定位死锁的循环等待链条,最终通过统一资源访问顺序、优化索引、控制事务粒度等方式,打破死锁的必要条件,从根因上解决问题。

目录
相关文章
|
6天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
18005 12
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
17天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
29545 141
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
7天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4606 20
|
6天前
|
人工智能 API 开发者
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
阿里云百炼Coding Plan Lite已停售,Pro版每日9:30限量抢购难度大。本文解析原因,并提供两大方案:①掌握技巧抢购Pro版;②直接使用百炼平台按量付费——新用户赠100万Tokens,支持Qwen3.5-Max等满血模型,灵活低成本。
1448 3
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案