线上业务突然抛出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默认)
这是死锁问题的核心重灾区,必须精准理解每一种锁的生效规则:
- 记录锁(record lock) :仅锁住索引中的某一行具体记录,只在
唯一索引+精准匹配的场景下生效,比如where id=1(id为主键),仅锁住id=1的行,对其他行完全无影响。 - 间隙锁(gap lock) :锁住索引记录之间的间隙,仅用于防止幻读,核心规则:间隙锁之间完全兼容,仅与插入意向锁互斥。比如表中id有1、3、5,间隙分为
(-∞,1)、(1,3)、(3,5)、(5,+∞),执行select * from t where id=2 for update会锁住(1,3)间隙,禁止任何插入该区间的操作,但其他事务可以同时持有该间隙的间隙锁。 - 临键锁(next-key lock) :innodb rr级别下默认的行锁算法,由「记录锁+该记录左边的间隙锁」组成,左开右闭区间,比如上述id的临键锁区间为
(-∞,1]、(1,3]、(3,5]、(5,+∞]。仅当查询条件为唯一索引+精准匹配时,临键锁会退化为记录锁,其他场景均为临键锁。 - 插入意向锁(insert intention lock) :一种特殊的间隙锁,
insert语句插入前会先申请该锁,它与间隙锁互斥,与其他插入意向锁兼容,是间隙锁场景下死锁的核心诱因。
1.2 锁等待与死锁的核心区别
很多开发者会将两者混为一谈,实际上二者的触发逻辑、处理机制完全不同:

二、锁等待的根因拆解与全链路排查方法 论
2.1 锁等待的高频根因
- 长事务未提交占用行锁:事务执行完更新语句后未及时提交/回滚,长期持有行锁,导致后续所有操作该行的事务全部阻塞,是线上最常见的锁等待诱因。
- 索引失效导致行锁升级为全表锁:更新/删除语句的查询条件没有索引,或索引失效,innodb无法精准定位行,会走全表扫描并给所有行加临键锁,相当于锁全表,任何更新操作都会被阻塞。
- 热点行并发更新:秒杀、库存扣减等场景,大量并发请求同时更新同一行记录,导致后续事务排队等待锁释放,出现大面积锁等待超时。
- 手动加锁范围过大:滥用
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:根因定位与应急处理
- 若阻塞源是未提交的长事务,可通过
kill 阻塞线程id临时释放锁,恢复业务; - 若为索引失效导致的全表锁,立即给查询条件添加合适的索引,确保更新语句走精准索引;
- 若为热点行更新,优化业务逻辑,采用分布式锁、队列削峰等方式控制并发度。
三、死锁的形成条件与高频场景复现
3.1 死锁形成的4个必要条件
死锁的触发必须同时满足以下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: 80805.2 死锁复现与排查
- 在mysql中创建
t_user表并插入初始数据; - 启动spring boot项目,访问
http://localhost:8080/swagger-ui.html,调用/user/deadlock/simulate接口,传入参数id1=1、id2=2、age1=21、age2=26; - 接口执行完成后,在mysql中执行
show engine innodb status,查看死锁日志; - 通过日志解读,定位到死锁根因为两个事务交叉更新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与索引优化
- 所有更新、删除语句必须通过
explain检查执行计划,确保走精准索引,避免全表扫描导致的全表锁; - 尽量使用精准匹配查询,避免大范围的范围查询,减少间隙锁的生效范围;
- 业务允许的情况下,将隔离级别调整为读已提交(rc),rc级别下无间隙锁,仅存在记录锁,可消除90%的间隙锁导致的死锁,同时半一致性读可大幅减少锁等待;
- 避免并发插入相同的唯一键值,减少唯一键冲突导致的s锁申请。
6.2 业务代码优化
- 所有并发更新场景,必须统一资源的访问顺序,比如按主键从小到大、按业务编码固定顺序更新,打破循环等待条件;
- 严格控制事务粒度,事务内的sql数量尽量最少,避免在事务内执行rpc调用、io操作等耗时逻辑,减少锁的持有时间;
- 避免滥用
select ... for update手动加锁,若必须使用,确保查询条件走精准索引,且锁定范围最小; - 优先使用编程式事务,避免声明式事务传播行为不当导致的大事务。
6.3 数据库配置优化
- 确保
innodb_deadlock_detect=on,开启死锁检测,默认开启; - 开启
innodb_print_all_deadlocks=on,将所有死锁日志打印到mysql错误日志,方便事后排查; - 高并发场景下,适当调小
innodb_lock_wait_timeout,避免长时间的业务阻塞; - 超高并发场景下,若死锁检测导致cpu占用过高,可通过分布式锁、队列削峰等方式控制并发度,避免直接关闭死锁检测。
总结
mysql锁等待与死锁的本质,是innodb锁机制下并发事务对资源的竞争形成的阻塞与循环等待。想要彻底解决这类问题,核心是先搞懂innodb锁的底层逻辑与生效规则,再通过show engine innodb status精准定位死锁的循环等待链条,最终通过统一资源访问顺序、优化索引、控制事务粒度等方式,打破死锁的必要条件,从根因上解决问题。
以上就是mysql锁等待与死锁根治的全攻略的详细内容,更多关于mysql锁等待与死锁根治的资料请关注代码网其它相关文章!
发表评论