当前位置: 首页 > backend >正文

出现了锁等待或死锁现象怎么办?乐观锁?分布式锁了解一下?

目录

一、问题分析

1.1按惯例贴异常:

二、问题定位 

2.1结合实际 

 2.2问题剖析

三、问题解答

3.1.加乐观锁

3.2.分布式锁。


一、问题分析

1.1按惯例贴异常:
187891478:618:46:21  187891478  X  RECORD  `*****`.`charge_order`  idx_charge_order  618  46  21  1, 0x99B18692C4, 2, 25, 771, 365

上面的异常可以通过mysql 语句:SHOW ENGINE INNODB STATUS可查到,它展示的是InnoDB 的锁信息。逐字逐句分析:

  • 187891478:618:46:21 是锁持有事务的表空间ID、页号和槽位。

  • 187891478 是事务ID。

  • X 表示该锁是 排他锁(Exclusive Lock)

  • RECORD 表示锁的是 记录锁

  • ***.charge_order 是表名。

  • idx_charge_order 是锁对应的索引。

  • 后面的数字如 1, 0x99B18692C4, 2, 25, 771, 365 是被锁定的记录内容的键值(被加锁的索引键)。

二、问题定位 

 主要的问题

  • 多个事务加锁同一条记录

    • 多个事务持有相同页的锁(例如 618:46:21,被多个事务锁住):

      187891478 187891319 187891257 187831733

      都在 idx_charge_order 索引的同一位置上加了锁,极有可能是争抢同一条数据,导致锁等待。

  • 联合索引或非主键更新

    • idx_charge_order 表示加锁操作通过 非主键索引 进行的。可能是某个字段上的查询或更新导致了间隙锁记录锁

    • 使用非唯一索引更新数据时,MySQL 会加锁相关记录,以防止幻读。

  • 行锁 + GAP 锁混合引发的死锁

    • 如果你用的是 SELECT ... FOR UPDATE 或者更新时条件不是主键,很可能会在 InnoDB 的 Next-Key Locking 策略下加上间隙锁,进而引发死锁。

  • 事务未及时提交

    • 如果多个事务对同一条记录加锁,且长时间未提交,会造成阻塞,甚至死锁。

2.1结合实际 

 那根据我自己的项目分析主要是因为定时任务叠加请求同一个接口,这个接口用了  FOR UPDATE去查询而后这个接口再进行更新。

 public void processChargingNew(OrderEntity order) {TransactionStatus transactionStatus = null;try {// 查询并加锁 charge_order 表的记录LambdaQueryWrapper<ChargeOrderEntity> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(ChargeOrderEntity::getOrderId, order.getId()).eq(ChargeOrderEntity::getIsValid, Constants.VALID).last("LIMIT 1 FOR UPDATE"); // 限制只锁一行ChargeOrderEntity chargeOrderEntity = baseMapper.selectOne(queryWrapper);if (chargeOrderEntity == null) {log.warn("充电订单不存在,orderId={}", order.getId());return;}这边省略业务逻辑……// 更新订单状态为启动中updateChargeOrderStart(chargeOrderEntity.getOrderId(), station.getId(), chargeOrderEntity);
 2.2问题剖析

1.selectOne(... FOR UPDATE) 出现在事务开启前

ChargeOrderEntity chargeOrderEntity = baseMapper.selectOne(queryWrapper); // 含 FOR UPDATE
...
transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
 

FOR UPDATE 需要在数据库事务中执行才有效!在事务开始 之前 加的锁,这意味着该锁 不是在一个有效事务上下文中产生的,实际效果不可靠,甚至不同线程间产生锁等待却得不到释放,导致死锁。 

因为定时任务每3秒会执行一次导致多个线程同时调用 这个接口,并且他们抢的是同一个 order.getId(),那么他们都会尝试 FOR UPDATE 相同的记录,哪怕只加锁一行,也可能形成排他锁等待队列

  • 如果前一个事务迟迟不提交(比如卡在业务逻辑的网络通信或外部设备响应),其他线程将被阻塞。

  • 一旦另一个线程在等待过程中,也去锁别的资源(比如 Redis、充电站表等),就很容易出现“循环等待” → 死锁。

三、问题解答

我的方案措施:

3.1.加乐观锁
  • 如果只是为了防止并发更新同一条订单数据,可以用乐观锁(如版本号 version 字段 + where version = ?)来实现并发控制。这里有个细节就是项目必须是Spring Boot 集成 MyBatis-Plus,配置乐观锁插件。

  • Spring Boot + MyBatis-Plus 项目结构

    <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.x</version>
    </dependency>
    

    需要在 配置类 中添加一个 @Bean,如下

    @Configuration
    public class MyBatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
    }
    

    放在任何一个 @Configuration 类里都可以,比如项目里的 MyBatisPlusConfig.java 或者 PersistenceConfig.java 等。


    注意事项:

  • MyBatis-Plus 3.4.x 以前版本 用的是旧插件注册方式。

  • 新版 3.5.x 起,都统一使用 MybatisPlusInterceptor 插件机制。

  • 插件加载顺序重要,如果还有分页插件,也得放进去,比如:

interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

配好之后,使用方式不变 

@Version
private Integer version;

 代码示例:

// 查询并加锁 charge_order 表的记录
LambdaQueryWrapper<ChargeOrderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ChargeOrderEntity::getOrderId, order.getId()).eq(ChargeOrderEntity::getIsValid, Constants.VALID).last("LIMIT 1");// 去掉 FOR UPDATE,改用乐观锁ChargeOrderEntity chargeOrderEntity = baseMapper.selectOne(queryWrapper);

 只要调用 updateById() 等方法,MyBatis-Plus 自动帮你在 SQL 加上 version = ? 的条件并做自增。(大功告成!!)

3.2.分布式锁。

// 调整顺序:
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
try {
    // 查询并加锁必须放在事务内
    ChargeOrderEntity chargeOrderEntity = baseMapper.selectOne(
        new LambdaQueryWrapper<ChargeOrderEntity>()
            .eq(ChargeOrderEntity::getOrderId, order.getId())
            .eq(ChargeOrderEntity::getIsValid, Constants.VALID)
            .last("LIMIT 1 FOR UPDATE")
    );

    // 其余逻辑...
 

 如果是多实例部署(集群中多个服务节点同时跑这个定时任务)这种情况下就建议加上Redis 分布式锁,防止多个节点同时处理同一个订单。可以在任务开始时做类似以下锁控制:

String lockKey = "prepare:charge:order:" + order.getId();
boolean isLocked = redisUtils.tryLock(lockKey, 0, 30); // 不等待,锁30秒
if (!isLocked) {log.info("订单正在处理,跳过 orderId={}", order.getId());return null;
}try {// 执行业务逻辑} finally {redisUtils.releaseLock(lockKey);
}

这样可以防止集群中多个任务节点同时操作同一条订单。

http://www.xdnf.cn/news/1273.html

相关文章:

  • C语言教程(十三):C 语言中 enum(枚举)的详细介绍
  • C++ 学习指南
  • 一款强大的实时协作Markdown工具 | CodiMD 9.6K ⭐
  • 携程-酒旅-数据研发面经【附答案】
  • 【Spring】单例作用域下多次访问同一个接口
  • Discuz!+DeepSeek:传统论坛的智能化蜕变之路
  • 【C++】新手入门指南(下)
  • 《Linux TCP通信深度解析:实现高可靠工业数据传输客户端》
  • 使用Python设置excel单元格的字体(font值)
  • 笔记本电脑研发笔记:BIOS,Driver,Preloader详记
  • Win10一体机(MES电脑设置上电自动开机)
  • 《Android系统应用部署暗礁:OAT文件缺失引发的连锁崩溃与防御体系构建》
  • Mediatek Android13 设置Launcher
  • 基于ssm的疫情防控志愿者管理系统(源码+文档)
  • SpringBoot_为何需要SpringBoot?
  • AlmaLinux 9.5 调整home和根分区大小
  • 机器学习基础 - 分类模型之决策树
  • 深度学习--卷积神经网络数据增强
  • TP(张量并行)和EP(专家并行)的区别
  • C++学习之游戏服务器开发十二nginx和http
  • 从信息泄露到内网控制
  • STM32外部中断与外设中断区别
  • 数据结构——队列
  • 华为交换机命令笔记
  • 【springsecurity oauth2授权中心】将硬编码的参数提出来放到 application.yml 里 P3
  • C++23 中 static_assert 和 if constexpr 的窄化布尔转换
  • Agent智能体ReAct机制深度解读:推理与行动的完美闭环
  • 实战华为1:1方式1 to 2 VLAN映射
  • hbuilderx云打包生成的ipa文件如何上架
  • 发送百度地图的定位