在开始编写代码之前,我们需要确保本地环境已经安装了Redis,并且创建好一个基础的Spring Boot项目。为了避免环境配置问题导致卡壳,这里提供具体的安装命令和依赖配置。
如果你本地还没有Redis,最快的方式是使用Docker直接启动。打开终端,执行以下命令即可拉取并运行一个单机版Redis实例:
docker run -d -p 6379:6379 --name redis-secKill redis:6.2-alpine
执行完毕后,使用 redis-cli ping 命令,如果返回 PONG,则说明Redis已正常运行。
在你的Spring Boot项目的 pom.xml 文件中,添加 spring-boot-starter-data-redis 依赖。这是官方提供的整合包,不需要额外配置连接池。
```xml打开 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 ```在电商秒杀场景中,最大的痛点是“超卖”。如果先查库存再减库存,在并发环境下一定会出问题。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中加载并执行这个脚本。为了方便复用,我们将脚本加载逻辑放在Bean初始化阶段,并提供一个Service类供外部调用。
创建一个配置类 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创建 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代码写好了,如何验证它不会超卖?我们不能手动点刷新,必须使用多线程模拟并发抢购。下面提供一个可以直接在 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操作,导致吞吐量暴跌。
正确的做法是使用异步消息队列:
通过这套流程,我们实现了:
易频IT社区是综合性互联网IT技术门户网站,专注分享网络技术、服务器运维、网络安全、编程开发、系统架构、云计算、大数据等行业干货,实时更新IT行业资讯、零基础教程、实战案例,为IT从业者、技术爱好者提供专业的学习交流平台。
Copyright © 2021-2026 易频IT社区. All Rights Reserved. 备案号:闽ICP备2023013482号 网站地图