You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data访问MySQL时PESSIMISTIC LOCK失效问题排查

排查Spring Boot多实例定时任务重复选记录的问题

我帮你梳理几个关键的排查方向,这些都是导致悲观锁+SKIP_LOCKED不生效、多实例重复选取记录的常见原因:

1. 先确认MySQL版本是否支持SKIP_LOCKED

SKIP_LOCKED是MySQL 8.0.1及以上版本才引入的特性,如果你的数据库版本低于这个门槛,FOR UPDATE SKIP LOCKED语法完全不会生效,数据库根本不会执行"跳过已锁定记录"的逻辑,自然会出现多实例选到相同数据的情况。

你可以执行这条SQL确认版本:

SELECT VERSION();

如果版本低于8.0.1,要么升级MySQL,要么换用其他分布式锁方案(比如Redis锁+记录标记字段)。

2. 检查事务注解的生效情况(内部调用陷阱)

你的updateTrxStatus标注了@Transactional(Propagation.REQUIRES_NEW),但如果这个方法是被同一个类内部的其他方法调用(比如定时任务方法直接调用它,或者它内部用this.executeUsingNQ()调用其他方法),Spring的AOP代理会失效,REQUIRES_NEW的事务特性等于没加!

这会导致查询和更新不在同一个事务中:查询时加的锁会立即释放,其他实例就能瞬间查到相同的记录。

解决办法:

  • 把事务方法抽到独立的Service类中,通过Spring注入的方式调用(别用this.xxx)
  • 或者通过AopContext.currentProxy()获取代理对象来调用内部方法(需要开启expose-proxy=true)

3. 原生SQL必须添加固定排序规则

你的原生SQL没有ORDER BY子句:

select * from transaction t where t.status = ?1 limit ?2 for update SKIP LOCKED

MySQL在没有ORDER BY的情况下,LIMIT返回的结果集是不确定的(取决于存储引擎的扫描顺序)。哪怕有SKIP_LOCKED,两个实例也可能同时扫描到相同的一批记录,导致锁定重复数据。

必须添加固定的排序字段(比如主键id),确保每次查询的顺序一致:

select * from transaction t where t.status = ?1 order by t.id asc limit ?2 for update SKIP LOCKED

这样第一个实例会锁定前N条id最小的记录,第二个实例查询时会自动跳过这些已锁定的记录,转而获取下一批数据。

4. 验证JPA是否正确生成了带SKIP_LOCKED的SQL

开启SQL日志,查看实际执行的SQL语句是否包含FOR UPDATE SKIP LOCKED:
在application.properties中添加:

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.type.descriptor.sql=TRACE

运行程序后观察控制台输出:

  • 如果用@Lock+@QueryHint的方式,生成的SQL必须包含FOR UPDATE SKIP LOCKED
  • 如果用原生SQL,确认SKIP_LOCKED是否存在

如果用@QueryHint时没生成SKIP_LOCKED,需要调整两个配置:

  • 改用正确的Hibernate方言:必须用MySQL8Dialect而非MySQL5Dialect
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
    
  • 直接指定javax.persistence.lock.timeout的值为5(LockOptions.SKIP_LOCKED对应的数值),替代枚举转字符串:
    @QueryHints(value={@QueryHint(name = "javax.persistence.lock.timeout", value = "5")})
    

5. 确认事务的完整性

updateTrxStatus中,查询和更新必须在同一个事务内完成:

  • 确保executeUsingNQ的@Transactional(Propagation.REQUIRED)会加入updateTrxStatus的REQUIRES_NEW事务(前提是事务注解生效)
  • 不要手动提交事务,让Spring在方法结束时自动提交,确保锁直到更新完成后才释放

额外的手动验证步骤

你可以模拟两个实例的场景,快速验证锁是否生效:

  1. 在第一个实例的updateTrxStatus方法中,查询后、更新前添加Thread.sleep(10000),让事务保持打开状态
  2. 同时启动第二个实例执行相同逻辑
  3. 观察第二个实例的查询结果:如果跳过了第一个实例锁定的记录,说明锁逻辑生效;如果仍选到相同记录,说明锁根本没起作用,回到前面的排查点

内容的提问来源于stack exchange,提问作者Moustafa Essa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 17:22:29