出现了锁等待或死锁现象怎么办?乐观锁?分布式锁了解一下?
目录
一、问题分析
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);
}
这样可以防止集群中多个任务节点同时操作同一条订单。