当前位置: 代码网 > it编程>数据库>Mysql > MySQL锁等待与死锁根治的全攻略

MySQL锁等待与死锁根治的全攻略

2026年04月09日 Mysql 我要评论
线上业务突然抛出lock wait timeout exceeded异常、接口超时、甚至事务被强制回滚,90%以上的场景都源于mysql锁等待与死锁问题。很多开发者面对问题时无从下手,要么只会重启服务

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

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

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

1.1 锁的核心维度划分

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

根因分析:事务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
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的间隙锁阻塞,死锁触发

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

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

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

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

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

根因分析:唯一键冲突时,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精准定位死锁的循环等待链条,最终通过统一资源访问顺序、优化索引、控制事务粒度等方式,打破死锁的必要条件,从根因上解决问题。

以上就是mysql锁等待与死锁根治的全攻略的详细内容,更多关于mysql锁等待与死锁根治的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com