导读:本期聚焦于郭世昌创作的《Redisson分布式锁在Spring Boot中如何防止超卖?实战教程详解》,敬请观看详情。秒杀场景下库存超卖是最常见的并发问题,单纯依赖数据库更新或者Redis的setnx命令往往存在锁过期、误删他人锁、不可重入等隐患。本文围绕Redisson分布式锁展开,先分析超卖产生的根本原因,再对比常见加锁方案的缺陷,随后给出Spring Boot整合Redisson的完整步骤,包含依赖引入、配置类编写、RLock接口的tryLock使用示例以及看门狗自动续期机制的原理解读,最后总结了锁超时时间设置、解锁放置位置等实战避坑要点,帮助你写出真正健壮的库存扣减代码。

库存超卖问题几乎是从单机应用走向分布式架构时必然要面对的一道坎。在单体应用里,一个synchronized关键字就能保证扣减库存的线程安全,可一旦服务部署了多个实例,JVM级别的锁就彻底失效了,两个节点同时读到库存为1,各自扣减一次,超卖就这样发生了。本文以一个典型的秒杀下单接口为例,讲解如何用Redisson分布式锁在Spring Boot项目中解决这个问题。

Redisson分布式锁在Spring Boot中如何防止超卖?实战教程详解

一、超卖是怎么产生的:从一段有问题的代码说起

先看一段最常见的下单扣库存代码,很多项目的初版长这样:

public void createOrder(Long productId) {
    // 第一步:查询库存
    Integer stock = stockMapper.getStock(productId);
    if (stock <= 0) {
        throw new BusinessException("库存不足");
    }
    // 第二步:扣减库存
    stockMapper.decreaseStock(productId);
    // 第三步:创建订单
    orderMapper.insert(buildOrder(productId));
}

这段代码在单机低并发下没有任何问题,但它的致命之处在于"查"和"减"是两步操作,不具备原子性。假设库存只剩1件,线程A和线程B同时执行到查询这一行,都拿到了stock等于1,都通过了校验,接着各自执行扣减,库存就变成了-1,这就是超卖的直接成因。

把服务部署多个实例后,即使你在方法上加@Synchronized或者JUC的锁,也只能锁住当前JVM内的线程。负载均衡把两个请求分发到两台机器上,两把"本地锁"互不可见,形同虚设。这就是必须引入分布式锁的原因:把锁放在所有实例都能访问的第三方介质上,这里自然首选Redis。

二、为什么不用setnx手写锁,而要用Redisson

不少同学的第一反应是直接用Redis的SET key value NX EX seconds命令自己实现,这在原理上没问题,但生产环境会踩三个坑。第一个坑是锁过期时间不好估:设置太短,业务还没执行完锁就自动释放了,第二个请求趁虚而入;设置太长,持有锁的实例宕机后其他节点要干等很久。

第二个坑是误删别人的锁。线程A的锁过期后被线程B拿到,A执行完后执行DEL把B的锁删掉了,C又进来了,锁等于没加。虽然可以用Lua脚本先比较value再删除来规避,但代码复杂度明显上升。第三个坑是不可重入:同一个线程内嵌套调用加锁方法时会把自己堵死。

Redisson把这些坑全部处理掉了。它提供可重入锁RLock、看门狗自动续期机制、加锁解锁的原子性操作(底层是Lua脚本),并且和Spring容器无缝集成。对于绝大多数业务系统来说,用Redisson比手写锁可靠得多,也省心得多。

三、Spring Boot整合Redisson完整步骤

第一步引入依赖,以Maven为例,注意版本与你的Spring Boot版本和Redis部署模式相关,单机模式引入基础包即可:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>

第二步编写配置类。虽然starter支持在yml中配置,但用Java Config方式更灵活,方便后续切换集群模式:

@Configuration
public class RedissonConfig {

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 单机模式,三个参数依次是地址、数据库、连接池大小
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setDatabase(0)
              .setConnectionPoolSize(32);
        return Redisson.create(config);
    }
}

第三步在业务代码中使用RLock。核心是tryLock方法:带等待时间的重载表示最多等待多少秒去抢锁,抢不到就放弃;leaseTime表示锁的持有时间。改造后的下单逻辑如下:

@Service
public class OrderService {

    @Autowired
    private RedissonClient redissonClient;

    public void createOrder(Long productId) {
        // 锁的粒度要细到具体商品,避免不同商品互相阻塞
        RLock lock = redissonClient.getLock("stock:lock:" + productId);
        boolean locked = false;
        try {
            // 最多等待5秒,锁30秒后自动释放
            locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (!locked) {
                throw new BusinessException("当前抢购人数过多,请稍后重试");
            }
            Integer stock = stockMapper.getStock(productId);
            if (stock <= 0) {
                throw new BusinessException("库存不足");
            }
            stockMapper.decreaseStock(productId);
            orderMapper.insert(buildOrder(productId));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 只有持有锁的线程才能解锁,这里必须放在finally里
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

这里有几个细节值得展开。锁的key粒度要控制在商品维度,用商品ID拼接,这样不同商品的抢购互不影响,吞吐量比全局一把锁高出几个量级。解锁务必写在finally块中,并且加上isHeldByCurrentThread判断,防止在锁已经过期的情况下解锁抛出异常。

四、看门狗机制:不传leaseTime才有续期

上面代码里显式传了30秒的leaseTime,此时看门狗是不会生效的。如果你调用的是lock.tryLock(5, TimeUnit.SECONDS)这种不指定锁时长的重载,Redisson会启用看门狗:默认锁时长30秒,后台定时任务每10秒(即锁时长的三分之一)检查一次,只要持锁线程还活着就自动把锁续期到30秒,直到业务执行完主动解锁或进程崩溃。

这个机制解决了"锁过期但业务没执行完"的经典矛盾。看门狗的本质是在Redisson客户端维护了一个定时任务,依赖Netty的时间轮实现,进程一旦挂掉,续期任务也随之消失,锁会在30秒后自然过期,不会造成死锁。

实践中如何选择?如果业务执行时间相对稳定且可预估,显式指定leaseTime更简单直接;如果业务耗时波动大,比如内部还调用了第三方接口,建议交给看门狗管理。但要警惕一点:业务代码里如果有while死循环,看门狗会一直续期,锁就永远不释放,这属于业务bug而非框架问题。

五、实战避坑清单与方案延伸

分布式锁能防住并发读改写,但它不是银弹。以下几点经验供参考:

  • 锁内逻辑要轻:只把"查库存加扣库存"这段临界区放锁内,创建订单、发消息等操作尽量挪到锁外或异步执行,缩小锁粒度能显著提升并发能力。
  • 兜底方案是数据库:即使加了分布式锁,扣库存的SQL也应该写成UPDATE stock SET num = num - 1 WHERE product_id = ? AND num > 0,根据影响行数判断是否成功,双保险防极端情况。
  • 热点商品考虑锁替换:秒杀爆款商品的锁竞争依然激烈,可叠加Redis预扣库存加Lua脚本原子扣减,或用分段库存把一把锁拆成多把。
  • 主从切换风险:Redis主从架构下锁信息还没同步到从库时主节点宕机,可能出现两个客户端同时持锁。对正确性要求极高的场景可开启Redisson的RedLock或改用Zookeeper实现,需自行权衡复杂度。

总结一下,Redisson通过可重入锁、Lua脚本原子操作和看门狗续期,把手写分布式锁的各种隐患都封装妥当,配合Spring Boot的依赖注入用起来非常顺手。防超卖的正确姿势是:分布式锁控制并发进入临界区,数据库条件更新做最后兜底,两者结合才能扛住真实秒杀场景的考验。

Redisson分布式锁Spring Boot防超卖修改时间:2026-09-13 00:24:40

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/55648.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。