news 2026/5/12 3:07:40

Redis 无锁化库存扣减方案(INCR + SETNX 实现,高并发不超卖)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 无锁化库存扣减方案(INCR + SETNX 实现,高并发不超卖)

在高并发场景(如秒杀、抢购)中,库存扣减是核心业务,也是最容易出现问题的环节——超卖、锁竞争、死锁、不知道扣减对应哪个库存节点,这些都是开发者常踩的坑。

本文将分享一种Redis INCR + SETNX 组合的无锁化库存扣减方案,彻底解决上述问题,实现高并发下的安全扣减,同时清晰追踪每一次扣减对应的库存节点,适合博客分享、项目落地和面试参考。

一、方案背景与核心痛点

在传统库存扣减中,我们常遇到以下问题:

  • 用 Redis DECR 直接扣减:高并发下可能出现超卖(虽 Redis 单线程原子性,但多请求判断库存+扣减非原子操作);

  • 用 SETNX 做分布式锁:存在严重锁竞争,高并发下大量请求阻塞或失败,性能低下;

  • 多次扣减时,无法追踪每一次扣减对应的具体库存节点,排查问题困难;

  • 引入 Lua 脚本虽能解决问题,但部分开发者对 Lua 不熟悉,维护成本高。

基于此,我们采用「INCR 生成唯一序号 + SETNX 抢占令牌」的无锁思路,既保证高并发安全,又能清晰追踪每一次扣减,且无需复杂脚本,易于理解和落地。

二、核心原理(无锁化的关键)

方案的核心是「不竞争同一个库存 key,用唯一序号标记每一次扣减」,利用 Redis 原子命令的特性,实现无锁化、高并发、不超卖。

1. 核心命令解析

  • INCR 命令:Redis 原子自增命令,每次执行对指定 key 的值 +1,即使并发 10000+ 请求,也能保证生成的序号唯一、不重复、不乱序(Redis 单线程执行,天然原子性)。

  • SETNX 命令:Redis 分布式锁核心命令(SET if Not eXists),只有当 key 不存在时才会设置成功,用于抢占「库存令牌」,保证同一个序号不会被多个请求占用。

2. 无锁化逻辑流程

整个流程无需任何分布式锁,完全并行执行,步骤如下:

  1. 初始化商品库存:在 Redis 中存储商品的总库存(如商品 ID=1001,总库存=100);

  2. 请求到来时,通过 INCR 命令生成一个全局唯一的自增序号(序号从 1 开始,依次递增);

  3. 判断序号是否超过总库存:若序号 > 总库存,说明库存不足,扣减失败;

  4. 若序号 ≤ 总库存,用 SETNX 命令抢占该序号对应的「库存令牌」(key 格式固定,包含商品 ID 和序号);

  5. SETNX 抢占成功,说明扣减成功(该序号对应的库存被当前请求占用);抢占失败,说明该序号已被其他请求占用,扣减失败。

关键优势:每个请求对应唯一序号,抢占的是自己的「专属令牌」,不会和其他请求竞争同一个 key,实现真正的无锁化。

三、完整方案实现(Spring Boot + Redis)

以下是可直接落地的代码实现,基于 Spring Boot + Spring Data Redis,包含核心方法、配置、测试说明,复制即可使用。

1. 依赖配置(pom.xml)

引入 Redis 相关依赖,确保项目能正常操作 Redis:

<!-- Spring Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 连接池依赖(可选,提升性能) --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>

2. Redis 配置(application.yml)

配置 Redis 连接信息,确保能正常连接 Redis 服务:

spring: redis: host: 127.0.0.1 # 你的 Redis 地址 port: 6379 # Redis 端口 password: # 若有密码,填写密码 lettuce: pool: max-active: 100 # 最大连接数 max-idle: 20 # 最大空闲连接 min-idle: 5 # 最小空闲连接 max-wait: 1000ms # 最大等待时间

3. 核心业务代码(无锁库存扣减)

实现 INCR 生成序号、SETNX 抢占令牌的核心逻辑,清晰追踪每一次扣减对应的 key:

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; /** * Redis 无锁化库存扣减服务(INCR + SETNX 实现) * 高并发安全、不超卖、可追踪每一次扣减对应的库存节点 */ @Service public class StockDeductService { @Resource private StringRedisTemplate stringRedisTemplate; /** * 无锁化库存扣减核心方法 * @param productId 商品ID * @param totalStock 商品总库存(可从配置/数据库获取,此处简化为参数) * @return true:扣减成功;false:扣减失败(库存不足/令牌抢占失败) */ public boolean deductStock(Long productId, int totalStock) { // 1. 定义 Redis key:自增序号key、库存令牌key String seqKey = "stock:seq:" + productId; // 自增序号key(全局唯一) String tokenKeyPrefix = "stock:token:" + productId + ":"; // 库存令牌key前缀 try { // 2. INCR 生成唯一自增序号(原子操作,无并发问题) // 若 seqKey 不存在,Redis 会自动创建并设置值为 1,后续每次 INCR +1 Long seq = stringRedisTemplate.opsForValue().increment(seqKey); if (seq == null) { // 自增失败(Redis 异常),直接返回失败 return false; } // 3. 判断序号是否超过总库存:超过则库存不足,扣减失败 if (seq > totalStock) { return false; } // 4. 确定当前请求扣减的库存令牌key(核心:知道扣的是哪个key) String tokenKey = tokenKeyPrefix + seq; System.out.println("当前请求扣减的库存key:" + tokenKey); // 日志追踪 // 5. SETNX 抢占令牌(原子操作,只有key不存在时才设置成功) // 过期时间设置为10分钟,避免Redis中残留无效令牌(可根据业务调整) Boolean isSuccess = stringRedisTemplate.opsForValue() .setIfAbsent(tokenKey, "1", 10, TimeUnit.MINUTES); // 6. 抢占成功 = 扣减成功;抢占失败 = 该序号已被其他请求占用 return Boolean.TRUE.equals(isSuccess); } catch (Exception e) { // 异常处理(如Redis连接失败),可根据业务添加日志/重试逻辑 e.printStackTrace(); return false; } } /** * 初始化商品库存(项目启动时执行,或手动调用) * @param productId 商品ID * @param totalStock 商品总库存 */ public void initStock(Long productId, int totalStock) { String stockKey = "stock:total:" + productId; stringRedisTemplate.opsForValue().set(stockKey, String.valueOf(totalStock)); // 初始化自增序号key(可选,若不存在,INCR会自动创建) String seqKey = "stock:seq:" + productId; if (stringRedisTemplate.hasKey(seqKey)) { stringRedisTemplate.delete(seqKey); } } /** * 查询某一次扣减对应的库存令牌是否存在(用于追踪扣减记录) * @param productId 商品ID * @param seq 扣减序号 * @return true:存在(扣减成功);false:不存在(扣减失败/令牌过期) */ public boolean checkStockToken(Long productId, Long seq) { String tokenKey = "stock:token:" + productId + ":" + seq; return Boolean.TRUE.equals(stringRedisTemplate.hasKey(tokenKey)); } }

4. 测试代码(验证无锁化效果)

用多线程测试高并发场景,验证是否超卖、是否能追踪扣减的 key:

import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @SpringBootTest public class StockDeductTest { @Resource private StockDeductService stockDeductService; // 商品ID private static final Long PRODUCT_ID = 1001L; // 商品总库存 private static final int TOTAL_STOCK = 100; // 并发请求数 private static final int CONCURRENT_NUM = 200; @Test public void testDeductStock() throws InterruptedException { // 1. 初始化库存 stockDeductService.initStock(PRODUCT_ID, TOTAL_STOCK); // 2. 多线程并发测试 ExecutorService executorService = Executors.newFixedThreadPool(20); CountDownLatch countDownLatch = new CountDownLatch(CONCURRENT_NUM); // 统计扣减成功次数 AtomicInteger successCount = new AtomicInteger(0); for (int i = 0; i < CONCURRENT_NUM; i++) { executorService.submit(() -> { try { boolean result = stockDeductService.deductStock(PRODUCT_ID, TOTAL_STOCK); if (result) { successCount.incrementAndGet(); } } finally { countDownLatch.countDown(); } }); } // 等待所有线程执行完毕 countDownLatch.await(); executorService.shutdown(); // 3. 输出测试结果 System.out.println("并发请求数:" + CONCURRENT_NUM); System.out.println("扣减成功次数:" + successCount.get()); System.out.println("商品总库存:" + TOTAL_STOCK); System.out.println("是否超卖:" + (successCount.get() > TOTAL_STOCK ? "是" : "否")); } }

四、关键细节与优势(博客重点强调)

1. 如何知道扣减的是哪一个 key?

这是本文方案的核心优势之一,无需猜测,直接通过「自增序号」定位扣减的 key:

  • 每一次请求通过 INCR 拿到唯一序号 seq;

  • 库存令牌 key 格式为:stock:token:{商品ID}:{seq}

  • 例如:seq=5 → 扣减的 key 是stock:token:1001:5,通过checkStockToken方法可直接查询该扣减是否有效。

2. 方案核心优势

  • 真正无锁化:不使用分布式锁,所有请求并行执行,无锁竞争、无阻塞,性能远超 SETNX 分布式锁方案;

  • 绝对不超卖:INCR 生成唯一序号,序号超过总库存则拒绝扣减,SETNX 保证同一个序号不被重复占用;

  • 可追踪性强:每一次扣减都对应唯一的 key,便于排查问题、统计扣减记录;

  • 易于实现和维护:无需复杂的 Lua 脚本,仅用 Redis 两个基础原子命令,代码简洁,新手也能快速上手;

  • 高并发适配:支持万级并发请求,性能接近 Redis 原生命令,适合秒杀、抢购等高频场景。

3. 注意事项(避坑重点)

  • 自增序号 key 的过期时间:无需设置过期时间,若商品下架/库存重置,手动删除该 key 即可;

  • 库存令牌 key 的过期时间:建议设置 5~10 分钟,避免 Redis 中残留大量无效令牌,占用内存;

  • 异常处理:Redis 连接失败、网络波动等异常,需添加日志和重试逻辑(可结合 Spring 重试注解);

  • 库存一致性:若需要和数据库保持一致,可在扣减成功后,异步同步到数据库(最终一致性,不影响高并发性能)。

五、方案对比(面试/选型参考)

为了让读者更清晰地了解本方案的优势,对比主流库存扣减方案:

方案

核心命令

是否无锁

是否超卖

可追踪扣减 key

性能

维护成本

DECR 直接扣减

DECR

可能超卖

SETNX 分布式锁

SETNX + DECR

否(有锁竞争)

Lua 原子脚本

Lua + DECRBY

极高

中(需懂 Lua)

INCR + SETNX(本文方案)

INCR + SETNX

是(真正无锁)

六、总结与拓展

本文提出的 Redis INCR + SETNX 无锁化库存扣减方案,核心是「用唯一序号标记每一次扣减,用 SETNX 抢占令牌」,既解决了高并发超卖问题,又实现了扣减记录的可追踪,同时具备易于实现、维护成本低的优势。

适合场景:秒杀、抢购、限量商品发放等高并发库存扣减场景,尤其适合对代码简洁性、可追踪性有要求的项目。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/12 3:04:38

分数阶傅里叶变换在声纳阵列分析中的应用与优化

1. 分数阶傅里叶变换在声纳阵列分析中的核心价值在水下声学工程领域&#xff0c;准确计算声纳阵列的辐射模式一直是个技术难点。传统FFT算法虽然计算效率高&#xff0c;但在处理特定方位角的辐射特性时存在明显的精度局限。2005年日本防卫厅技术研究本所的这项研究&#xff0c;…

作者头像 李华
网站建设 2026/5/12 3:04:38

全栈AI智能体开发实战:基于LangGraph与Next.js的工程化模板解析

1. 项目概述&#xff1a;一个全栈AI智能体模板的诞生 最近在GitHub上看到一个挺有意思的项目&#xff0c;叫 vstorm-co/full-stack-ai-agent-template 。光看名字&#xff0c;你可能会觉得这又是一个“AI全栈”的缝合怪&#xff0c;或者是一个过度包装的概念。但作为一个在AI…

作者头像 李华
网站建设 2026/5/12 2:56:32

DRAM电荷恢复延迟优化与PaCRAM技术解析

1. DRAM电荷恢复延迟&#xff1a;性能瓶颈与优化契机现代计算机系统中&#xff0c;DRAM&#xff08;动态随机存取存储器&#xff09;的性能表现直接影响着整体系统的运行效率。DRAM单元通过电容存储电荷来表示数据&#xff0c;但这种存储方式存在一个根本性缺陷——电容会随时间…

作者头像 李华
网站建设 2026/5/12 2:54:44

Java集成Gemma大模型:本地推理与生产部署实战指南

1. 项目概述&#xff1a;当Gemma遇上Java 最近在开源社区里&#xff0c;一个名为 mukel/gemma4.java 的项目引起了我的注意。光看这个标题&#xff0c;熟悉AI模型和Java生态的朋友可能已经会心一笑。没错&#xff0c;这个项目直指一个核心痛点&#xff1a;如何让Google最新推…

作者头像 李华