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

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

目录

一、问题分析

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

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

相关文章:

  • 前端笔记-Vue3(中)
  • 输入框仅支持英文、特殊符号、全角自动转半角 vue3
  • Sqlserver安全篇之_Sqlcmd命令使用windows域账号认证sqlserver遇到问题如何处理的案例
  • JVM考古现场(二十四):逆熵者·时间晶体的永恒之战
  • 乐视系列玩机---乐视1 x600系列线刷救砖以及刷写第三方twrp 卡刷第三方固件步骤解析
  • 【AI News | 20250422】每日AI进展
  • Java 静态内部类面试题与高质量答案合集
  • 华为仓颉编程语言基础概述
  • 【漫话机器学习系列】215.处理高度不平衡数据策略(Strategies For Highly Imbalanced Classes)
  • 性能比拼: Redis vs Dragonfly
  • 服装印花/印烫环节计算机视觉应用设计方案
  • STL C++详解——priority_queue的使用和模拟实现 堆的使用
  • jenkins pipeline ssh协议报错处理
  • 【MCP Node.js SDK 全栈进阶指南】初级篇(4):MCP工具开发基础
  • 【MCP Node.js SDK 全栈进阶指南】初级篇(3):MCP资源开发基础
  • JavaScript ?? 运算符详解
  • 宏碁笔记本电脑怎样开启/关闭触摸板
  • 最新项目笔记
  • Qt Creator 创建 Qt Quick Application一些问题
  • C++:STL模板
  • 广西人饮旱情仍持续发展,桂西北、桂中风险较高
  • 为青少年写新中国成立的故事,刘统遗著《火种》出版
  • 新任遂宁市委副书记王忠诚已任市政府党组书记
  • 从沙漠到都市:贝亲世界地球日特别行动,以桃叶冰爽力开启地球降温之旅
  • 江南大部、江淮南部等地今起有较强降雨,水利部部署防范工作
  • 为什么要研制大型水陆两栖飞机?AG600总设计师给出答案