当前位置:网站首页 >  教程

电商秒杀系统Redis库存扣减零门槛实操指南

时间:2026年05月23日 01:36:48 来源:易频IT社区

一、环境准备与依赖引入

在开始编写代码之前,我们需要确保本地环境已经安装了Redis,并且创建好一个基础的Spring Boot项目。为了避免环境配置问题导致卡壳,这里提供具体的安装命令和依赖配置。

1. 安装并启动Redis

如果你本地还没有Redis,最快的方式是使用Docker直接启动。打开终端,执行以下命令即可拉取并运行一个单机版Redis实例:

docker run -d -p 6379:6379 --name redis-secKill redis:6.2-alpine

执行完毕后,使用 redis-cli ping 命令,如果返回 PONG,则说明Redis已正常运行。

2. 添加Maven依赖

在你的Spring Boot项目的 pom.xml 文件中,添加 spring-boot-starter-data-redis 依赖。这是官方提供的整合包,不需要额外配置连接池。

```xml org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 ```

3. 配置Redis连接信息

打开 src/main/resources/application.yml,添加以下配置。这里直接使用lettuce连接池,确保高并发下的连接复用。

```yaml spring: redis: host: 127.0.0.1 port: 6379 lettuce: pool: max-active: 20 max-idle: 10 min-idle: 5 ```

二、核心Lua脚本编写

在电商秒杀场景中,最大的痛点是“超卖”。如果先查库存再减库存,在并发环境下一定会出问题。Redis的Lua脚本可以保证“查询+扣减”这两个操作的原子性,中间不会被其他线程插队。

在项目的 src/main/resources 目录下创建一个名为 secKill.lua 的文件,并写入以下代码:

```lua -- 参数说明: -- KEYS[1]: 库存Key -- ARGV[1]: 扣减的数量(通常为1) local stockKey = KEYS[1] local deductQty = tonumber(ARGV[1]) -- 1. 获取当前库存 local stock = tonumber(redis.call('get', stockKey)) -- 2. 判断库存是否存在 if stock == nil then return -1 -- 库存未初始化 end -- 3. 判断库存是否充足 if stock < deductQty then return 0 -- 库存不足 end -- 4. 扣减库存 local remainStock = redis.call('decrby', stockKey, deductQty) return remainStock -- 返回剩余库存 ```

这段脚本逻辑非常清晰:先拿库存,没库存直接返回错误;有库存则判断够不够减,不够减返回0;够减就执行 decrby 命令并返回剩余值。

三、Java代码实现

电商秒杀系统Redis库存扣减零门槛实操指南

接下来我们需要在Java中加载并执行这个脚本。为了方便复用,我们将脚本加载逻辑放在Bean初始化阶段,并提供一个Service类供外部调用。

1. 配置DefaultRedisScript

创建一个配置类 RedisConfig.java,用于读取Lua文件并封装成 DefaultRedisScript 对象。这一步是必须的,否则每次请求都要读取文件,性能极差。

```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scripting.support.ResourceScriptSource; import java.util.List; @Configuration public class RedisConfig { @Bean public DefaultRedisScript secKillScript() { DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("secKill.lua"))); redisScript.setResultType(Long.class); return redisScript; } } ```

2. 编写秒杀Service

创建 SecKillService.java,注入 StringRedisTemplate 和我们刚才配置好的 DefaultRedisScript

```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; @Service public class SecKillService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private DefaultRedisScript secKillScript; // 预热库存,实际业务中通常在活动开始前由后台任务设置 public void initStock(String key, int amount) { stringRedisTemplate.opsForValue().set(key, String.valueOf(amount)); } / 执行秒杀扣库存 @param key 商品的库存Key @param buyNum 购买数量 @return 剩余库存,如果返回0表示库存不足,-1表示Key不存在 / public long doSecKill(String key, int buyNum) { // Lua脚本只支持Keys和Args两个参数列表,且Keys必须用List传递 List keys = Collections.singletonList(key); // 执行脚本 Long result = stringRedisTemplate.execute( secKillScript, keys, String.valueOf(buyNum) ); return result != null ? result : -1; } } ```

四、并发压测验证

代码写好了,如何验证它不会超卖?我们不能手动点刷新,必须使用多线程模拟并发抢购。下面提供一个可以直接在 main 方法中运行的测试代码。

创建一个测试类 SecKillTest.java

```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @SpringBootApplication public class SecKillTest { public static void main(String[] args) throws InterruptedException { // 启动Spring容器 ConfigurableApplicationContext context = SpringApplication.run(SecKillTest.class, args); SecKillService secKillService = context.getBean(SecKillService.class); String productKey = "product:1001:stock"; int totalStock = 100; int threadCount = 200; // 模拟200人同时抢 // 1. 初始化库存 secKillService.initStock(productKey, totalStock); System.out.println("库存初始化完成,当前库存: " + totalStock); // 2. 创建线程池 ExecutorService executorService = Executors.newFixedThreadPool(threadCount); // 3. 使用CountDownLatch让所有线程同时开始 CountDownLatch latch = new CountDownLatch(1); for (int i = 0; i < threadCount; i++) { executorService.submit(() -> { try { latch.await(); // 等待发令枪响 long remain = secKillService.doSecKill(productKey, 1); if (remain >= 0) { System.out.println(Thread.currentThread().getName() + " 抢购成功,剩余库存: " + remain); } else { System.out.println(Thread.currentThread().getName() + " 抢购失败"); } } catch (Exception e) { e.printStackTrace(); } }); } // 4. 发令枪响,开始抢购 long start = System.currentTimeMillis(); latch.countDown(); executorService.shutdown(); // 简单等待所有线程结束(实际生产环境不建议用这种方式) while (!executorService.isTerminated()) {} System.out.println("抢购结束,最终剩余库存: " + secKillService.doSecKill(productKey, 0)); System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms"); context.close(); } } ```

运行上述代码,观察控制台输出。你会发现,虽然有200个线程并发请求,但只有100个线程会打印“抢购成功”,且最终剩余库存会准确显示为0。如果出现负数,说明代码逻辑有误,但在上述Lua脚本的保护下,这是不可能发生的。

五、数据一致性落盘方案

解决了Redis的并发扣减问题后,还需要考虑数据持久化。Redis只是缓存,最终数据必须同步到MySQL数据库。切记,绝对不要在Redis扣减成功后同步调用MySQL保存订单,这会拖慢Redis操作,导致吞吐量暴跌。

正确的做法是使用异步消息队列:

  1. 发送消息:在Lua脚本执行成功后,将用户ID和商品ID封装成一条消息,发送到Kafka或RocketMQ。
  2. 消费消息:编写一个消费者服务,监听该队列,拿到消息后执行MySQL的 INSERT 订单操作和 UPDATE 库存操作。
  3. 兜底逻辑:如果MySQL插入失败(如数据库死锁),需要记录日志并利用重试机制进行补偿。

通过这套流程,我们实现了:

  • 高并发:利用Redis单线程模型和Lua原子性,抗住流量。
  • 不超卖:Redis层面的库存是唯一真理。
  • 数据最终一致:通过MQ解耦,异步落库。
标签 电商活动

相关推荐

最新

热门

推荐

精选

标签

易频IT社区是综合性互联网IT技术门户网站,专注分享网络技术、服务器运维、网络安全、编程开发、系统架构、云计算、大数据等行业干货,实时更新IT行业资讯、零基础教程、实战案例,为IT从业者、技术爱好者提供专业的学习交流平台。

Copyright © 2021-2026 易频IT社区. All Rights Reserved. 备案号:闽ICP备2023013482号 网站地图