FEATURED · 精选文章

线上问题复盘:分布式锁 + @Transactional 组合失效?

发布时间 / 2026/8/1 20:58:57
来源 / 创域科博编辑部
栏目 / 资讯中心
线上问题复盘:分布式锁 + @Transactional 组合失效? 摘要本文深入剖析了 Spring Boot 项目中分布式锁与 Transactional 事务组合使用时因锁提前释放而导致的并发数据不一致问题。通过对比问题版本与修复版本的时序差异揭示了 Spring AOP 执行顺序的陷阱并提供了两种有效的解决方案服务层拆分与编程式事务帮助开发者避免类似的高并发坑。一问题场景用户并发操作某个资源比如领取限量优惠券或者高频修改某个配置。这种防并发超卖的场景老 Java 开发闭着眼睛都能想到一套组合拳Spring Boot Transactional Redisson 分布式锁。当时同事咔咔一顿敲代码写得极其丝滑本地单线程测了没毛病经过 CodeReview 后直接提测上线。结果上线第一天运营跑过来说“后台数据不对啊怎么同一个资源被重复扣了两次”同事当时赶紧去看日志。不看不知道一看麻了虽然加了分布式锁但在极高并发下锁竟然“失效”了今天给大家复盘一下这个差点让同事背绩效 C 的坑也提醒大家 CodeReview 时多关注这类时序问题。二代码复现为了直观我把当时的业务代码简化一下。大体逻辑是这样的查询数据库中的剩余量 - 判断是否足够 - 扣减 - 保存。Java 代码示例问题版本Service public class ResourceServiceImpl implements ResourceService { Autowired private RedissonClient redissonClient; Autowired private ResourceMapper resourceMapper; Override Transactional(rollbackFor Exception.class) public void consumeResource(String resourceId) { String lockKey lock:resource: resourceId; RLock lock redissonClient.getLock(lockKey); try { // 尝试获取锁最多等3秒 if (lock.tryLock(3, TimeUnit.SECONDS)) { // 1. 查数据库当前库存 Resource res resourceMapper.selectById(resourceId); if (res.getStock() 0) { // 2. 扣减 res.setStock(res.getStock() - 1); resourceMapper.updateById(res); } else { throw new RuntimeException(库存不足); } } else { throw new RuntimeException(系统繁忙请重试); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 3. 释放锁 if (lock.isLocked() lock.isHeldByCurrentThread()) { lock.unlock(); } } } }可以直接看下这代码是不是很眼熟Transactional保证事务try...finally保证锁必定释放。这代码经过 CodeReview 时逻辑上看起来都没毛病啊但是并发一上来数据库的库存还是出现了负数。三问题分析-锁释放与事务提交的时差为了更直观地理解这个问题下面通过时序图对比问题版本和修复版本的关键执行顺序从上图可以清晰地看到问题版本线程A在第5步就释放了锁但事务提交在第9步。线程B在第6-8步拿到锁并操作数据库时看到的是未提交的老数据导致脏写。修复版本线程A在第5步先提交事务然后在第6步释放锁。线程B必须等待锁释放后才能操作看到的是已提交的新数据。排查了大半天抓了 MySQL 的 binlog 和 Redis 的执行日志对比终于发现了问题所在Spring AOP 的执行顺序问题。大家回忆一下Transactional的底层原理。Spring 是通过 AOP 动态代理来实现事务的相当于在你的业务方法外层包了一层Spring 代理类的伪代码// Spring 代理类的伪代码 public void proxyConsumeResource(String resourceId) { // 1. 开启数据库事务 connection.setAutoCommit(false); try { // 2. 执行你的真实业务逻辑包含加锁、改数据、释放锁 target.consumeResource(resourceId); // 3. 提交事务 connection.commit(); } catch (Exception e) { connection.rollback(); } }看出致命问题了吗在我的业务代码里finally块执行了lock.unlock()此时分布式锁已经被释放了。但是此时target.consumeResource()方法才刚刚执行完Spring 代理类的connection.commit()还没执行也就是说锁已经没了但数据还没落盘。这时候如果有另一个线程线程B并发打进来线程B看到 Redis 里没有锁顺利拿到锁。线程B去查数据库。因为线程A的事务还没 commit线程B查到的还是老数据线程B拿着老数据做扣减。线程A提交事务线程B紧接着也提交事务。完美脏写产生了数据被彻底打穿。四解决方案找到原因后解决起来就非常简单了。核心思想只有一个必须保证事务提交之后再释放锁。方案一粗暴拆分直接把加锁的逻辑往上提放到 controller 层或者再包一层 service。确保锁的范围大于事务的范围。Java 代码示例修复版本Service public class ResourceLockService { Autowired private RedissonClient redissonClient; Autowired private ResourceServiceImpl resourceService; // 注入原来的事务Service public void safeConsume(String resourceId) { String lockKey lock:resource: resourceId; RLock lock redissonClient.getLock(lockKey); try { if (lock.tryLock(3, TimeUnit.SECONDS)) { // 调用事务方法由于事务方法是一个独立的 proxy // 执行完毕返回到这里时事务已经 commit 啦 resourceService.consumeResource(resourceId); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isLocked() lock.isHeldByCurrentThread()) { lock.unlock(); } } } }注注意不要在同一个类里写这两个方法直接 this 调用会导致 AOP 失效老生常谈了。方案二编程式事务如果你不想多写一层类可以使用TransactionTemplate手动控制事务的边界。Java 代码示例// 在获取锁之后执行 transactionTemplate.execute(status - { // 查库、扣减、更新 return null; }); // 事务提交完毕再进入 finally 释放锁其它问题Redisson 看门狗失效问题借着这个机会再分享一个很多人用 Redisson 容易踩的坑。有些喜欢直接使用vibe coding会在加锁的时候传 leaseTime锁过期时间错误示例// 试图加锁等待3秒锁定10秒后自动释放 lock.tryLock(3, 10, TimeUnit.SECONDS);一旦你显式传入了leaseTimeRedisson 的 WatchDog看门狗机制就会失效如果你的业务逻辑执行时间超过了 10 秒比如发生了 Full GC 或者调了很慢的第三方接口锁会自动释放其他线程就会趁虚而入。正确做法是如果不知道业务具体执行多久千万别传leaseTime正确示例// 只传等待时间不传 leaseTime看门狗机制生效会自动帮你续期 lock.tryLock(3, TimeUnit.SECONDS);总结与建议平时的业务开发中Transactional和各种锁包括本地锁synchronized和分布式锁一起用的时候一定要多留个心眼画一画它们的作用域边界。核心要点回顾事务与锁的顺序确保锁的生命周期完全覆盖事务的生命周期即先加锁后开启事务先提交事务后释放锁。方案选择优先推荐方案一粗暴拆分逻辑清晰职责分离。方案二编程式事务适合简单场景。Redisson 使用避免随意指定leaseTime充分利用看门狗自动续期机制。测试验证高并发场景务必进行压测并结合日志和监控观察锁与事务的时序。有时候真不是底层组件不行纯粹是我们没把 Spring AOP 的执行顺序盘明白。希望这次的血泪教训能帮大家少掉几根头发。大家在线上还踩过什么离谱的并发问题欢迎评论区交流
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻