Spring并发锁失效实战排查:@Transactional锁穿透导致超卖问题根治方案

Spring并发锁失效实战排查:@Transactional锁穿透导致超卖问题根治方案

一、线上故障现象
线上秒杀活动、商品库存扣减业务出现严重超卖问题,库存数量为负,订单数量超出活动限定库存。测试环境单机测试、单线程模拟完全正常,无超卖现象,仅在生产高并发多线程场景下复现,故障概率随并发量提升持续升高。
QQ20260813-210356.png
业务监控显示:单次秒杀峰值并发500+,数据库库存字段频繁出现负数,每日产生数十笔无效超卖订单,需要人工订正数据,严重影响营销活动稳定性与数据准确性。最初怀疑是数据库无锁导致,添加synchronized锁后,问题依旧存在。

二、问题根源与错误代码分析
经过多轮压测复现、代码逐行排查、事务机制拆解,最终定位核心问题:Spring事务注解与Java同步锁执行顺序冲突,导致锁失效。

底层核心原理:Spring AOP动态代理机制中,事务开启、提交/回滚在方法外层执行,锁在方法内部执行。具体执行顺序:代理类开启事务 - 进入方法获取锁 - 执行业务扣减库存 - 释放锁 - 代理类提交事务。锁释放早于事务提交,高并发场景下,上一个线程释放锁后、事务未提交前,下一个线程获取锁进入方法,读取到未提交的旧库存数据,最终导致超卖。

线上导致超卖的错误核心代码:
/**

  • 错误代码:事务内部加锁,锁失效导致超卖
  • 核心问题:锁释放早于事务提交,并发穿透锁间隙
    */
    @Service
    public class StockServiceImpl implements StockService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized boolean deductStock(Long goodsId, Integer num) {
    // 1. 查询当前库存
    GoodsStock stock = stockMapper.selectById(goodsId);
    if (stock == null || stock.getStockNum() < num) {
    return false;
    }
    // 2. 扣减库存
    stock.setStockNum(stock.getStockNum() - num);
    // 3. 更新库存
    stockMapper.updateById(stock);
    return true;
    }
    }
    上述代码看似无问题,事务保证数据一致性,synchronized保证线程安全,但受Spring事务代理机制影响,锁完全失效,高并发必然超卖。

三、底层机制深度拆解
1. Spring事务执行链路:调用业务方法 - AOP前置拦截 - 开启数据库事务 - 执行目标方法(加锁、扣库存、更新数据、释放锁) - 方法执行完毕 - AOP后置拦截 - 提交/回滚事务。

2. 并发漏洞核心:锁在方法内释放,事务在方法外提交,中间存在时间间隙。线程A执行完扣减逻辑、释放锁,但事务未提交,数据库库存数据未更新;此时线程B获取锁,查询到旧库存数据,重复扣减,形成超卖。

3. synchronized锁局限性:仅能锁住当前JVM进程内线程,分布式场景下即使锁生效,依然存在跨机器并发问题,无法适配线上分布式集群环境。

四、多层级解决方案(单机+分布式全覆盖

  1. 单机场景最优解决方案:锁外置,事务内置
    调整锁与事务的执行顺序,将synchronized锁加到事务方法外层,保证锁包裹完整事务流程,事务提交完成后再释放锁,彻底杜绝锁间隙并发问题。优化后代码:
    /**
  • 优化后单机代码:锁包裹完整事务,杜绝锁失效
    */
    @Service
    public class StockServiceImpl implements StockService {

    @Override
    public boolean deductStock(Long goodsId, Integer num) {
    // 锁外置:锁住整个事务执行流程
    synchronized (this) {
    return doDeductStock(goodsId, num);
    }
    }

    // 事务方法内置,被锁完整包裹
    @Transactional(rollbackFor = Exception.class)
    public boolean doDeductStock(Long goodsId, Integer num) {
    GoodsStock stock = stockMapper.selectById(goodsId);
    if (stock == null || stock.getStockNum() < num) {
    return false;
    }
    stock.setStockNum(stock.getStockNum() - num);
    stockMapper.updateById(stock);
    return true;
    }
    }

  1. 分布式场景终极方案:数据库悲观锁+乐观锁双重保障
    线上服务均为分布式集群部署,单机锁无法解决跨节点并发问题,采用数据库锁机制彻底根治超卖,适配所有线上场景:
    方案一:数据库悲观锁(适用于高并发秒杀场景),查询库存时加行级排他锁,禁止其他事务读取修改数据:
    -- 悲观锁查询:for update 加行锁
    SELECT stock_num FROM goods_stock WHERE goods_id = #{goodsId} FOR UPDATE;

方案二:数据库乐观锁(适用于普通库存扣减场景,性能更高),通过版本号控制,避免并发更新冲突:
-- 乐观锁更新:版本号校验,并发更新失败自动回滚
UPDATE goods_stock SET stock_num = stock_num - #{num}, version = version + 1
WHERE goods_id = #{goodsId} AND version = #{version} AND stock_num >= #{num};

  1. 高阶优化:Redis分布式锁兜底
    超高并发秒杀场景,数据库锁性能瓶颈明显,采用Redisson分布式锁,结合事务机制,实现高性能、高可靠的库存扣减,彻底解决分布式并发超卖问题。

五、避坑总结与开发规范
1. 绝对禁止在@Transactional注解方法内部加本地锁,锁释放早于事务提交,必然并发失效;
2. 单机并发场景:锁外置、事务内置,保证锁包裹完整事务生命周期;
3. 分布式场景:放弃本地锁,优先使用数据库锁、Redis分布式锁,适配集群环境;
4. 库存、金额、订单等核心数据更新,必须加并发控制机制,禁止裸写数据库更新逻辑。
优化上线后,线上秒杀、库存扣减业务彻底解决超卖问题,高并发压测1000次无异常,库存数据精准无误。


友情链接
凡尘博客
凡尘博客文章|凡尘博客文摘
凡尘影院
凡尘乡音|凡尘街坊
凡尘博客|雨落凡尘博客|羽落凡尘博客
凡尘博客|雨落凡尘博客|羽落凡尘博客


版权声明
本文为凡尘(雨落凡尘、羽落凡尘)原创技术文章,采用 CC BY-NC-ND 4.0 协议。未经作者授权,禁止商业转载、二次修改与私自搬运,非商业转载需注明作者及原文链接。更多技术干货、实战踩坑笔记、生活随笔可关注凡尘博客,持续更新高质量原创内容。

标签: none

添加新评论

  • 上一篇:
  • 下一篇: