Spring Boot + Redis 实现高QPS秒杀功能
·
整体思路
在秒杀活动场景中,高并发和库存超卖是两个核心挑战。使用 Redis 的内存操作速度以及原子性能力,可以高效地处理库存扣减与请求过滤,再配合 Java 后端进行异步落库、限流等操作,即可实现一个较为可靠的秒杀下单功能。下面给出一种常见的实现方案。
步骤
- 活动开始前:将秒杀商品的库存预加载到 Redis 中,使用
String或Hash结构存储。 - 秒杀请求到达:
- 令牌桶/漏桶等限流,防止瞬间流量压垮系统。
- 基于用户 ID + 商品 ID 做重复下单校验(可放在 Redis 或本地缓存中)。
- 利用 Redis Lua 脚本 原子地检查库存并扣减,返回是否成功。
- 成功者:将下单信息发送到消息队列(如 RocketMQ、RabbitMQ 或 Redis Stream),立即返回“排队中”或“成功”的提示。
- 异步消费者:从消息队列拉取订单数据,执行数据库落库操作(订单表、库存扣减记录等),确保最终一致性。
- 失败处理:库存不足或重复下单的请求直接返回“秒杀结束”或“已参与过”。
简化版时序图
用户 -> 限流器 -> 重复下单检查 -> Lua脚本(扣减Redis库存)
| 成功
v
发送MQ消息 -> 立即返回排队提示
|
v
消费者 -> 落库DB
为什么不直接使用分布式锁(如 Redisson)?
分布式锁也可以实现库存扣减,但在秒杀高并发场景下,性能远不如 Lua 脚本。Lua 脚本在单 Redis 实例上可以做到 O(1) 复杂度的原子操作,是最佳实践。
通过上述设计,利用 Redis 高速判断 + 原子扣减 + 异步解耦,可以有效支撑数十万甚至百万级的秒杀 QPS。实际部署时还要结合压测和监控,动态调整限流阈值和 MQ 消费能力。
秒杀功能详细Demo
以下提供一个完整的 Spring Boot + Redis 秒杀示例,重点展示如何使用 Redis setNx 短时标记(3秒)防止同一用户快速重复点击。代码包含:令牌桶限流(Redisson)、setNx 防重标记、Lua 脚本原子扣库存、异步落库(线程池模拟 MQ)。可直接复制运行。
项目结构
src/main/java/com/example/seckill/
├── SeckillApplication.java
├── controller/SeckillController.java
├── service/SeckillService.java
├── service/RedisStockManager.java
├── limiter/RedissonRateLimiter.java
├── dao/OrderDao.java
└── entity/SeckillOrder.java
src/main/resources/
├── application.yml
└── lua/seckill_stock.lua
1. 依赖(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>seckill-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.25.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 配置文件(application.yml)
spring:
redis:
host: localhost
port: 6379
database: 0
lettuce:
pool:
max-active: 20
server:
port: 8080
3. Lua 脚本(src/main/resources/lua/seckill_stock.lua)
-- KEYS[1] : 库存key
-- ARGV[1] : 本次扣减数量(通常为1)
local stock = redis.call('get', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
return 0
end
redis.call('decrby', KEYS[1], ARGV[1])
return 1
4. 实体类(SeckillOrder.java)
package com.example.seckill.entity;
import java.util.Date;
public class SeckillOrder {
private Long id;
private Long userId;
private Long productId;
private Date createTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}
5. 模拟 DAO(OrderDao.java)
package com.example.seckill.dao;
import com.example.seckill.entity.SeckillOrder;
import org.springframework.stereotype.Repository;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Repository
public class OrderDao {
private final ConcurrentHashMap<Long, SeckillOrder> orderTable = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public boolean insertOrder(SeckillOrder order) {
long id = idGenerator.getAndIncrement();
order.setId(id);
orderTable.put(id, order);
return true;
}
// 检查该用户是否已成功秒杀过该商品(用于异步落库时的最终一致性)
public boolean existsByUserIdAndProductId(Long userId, Long productId) {
return orderTable.values().stream()
.anyMatch(o -> o.getUserId().equals(userId) && o.getProductId().equals(productId));
}
}
6. Redisson 令牌桶限流器(RedissonRateLimiter.java)
package com.example.seckill.limiter;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RedissonRateLimiter {
@Autowired
private RedissonClient redissonClient;
/**
* 尝试获取令牌(非阻塞,立即返回)
* @param key 限流器key
* @param rate 每秒生成的令牌数
* @param capacity 桶容量
* @return true-获取成功,false-被限流
*/
public boolean tryAcquire(String key, double rate, int capacity) {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// trySetRate 只会设置一次,后续调用不会覆盖已有配置
rateLimiter.trySetRate(RateType.OVERALL, rate, capacity, RateIntervalUnit.SECONDS);
return rateLimiter.tryAcquire(1);
}
}
7. Redis 库存管理器(RedisStockManager.java)
package com.example.seckill.service;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;
@Component
public class RedisStockManager {
private final RedisTemplate<String, Object> redisTemplate;
private DefaultRedisScript<Long> stockScript;
public RedisStockManager(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@PostConstruct
public void init() {
stockScript = new DefaultRedisScript<>();
stockScript.setResultType(Long.class);
stockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/seckill_stock.lua")));
}
/**
* 原子扣减库存
* @param stockKey 库存key
* @param num 扣减数量
* @return true-成功,false-库存不足
*/
public boolean decrStock(String stockKey, int num) {
Long result = redisTemplate.execute(stockScript, Collections.singletonList(stockKey), String.valueOf(num));
return result != null && result == 1L;
}
/**
* 预热库存(秒杀开始前调用)
*/
public void initStock(Long productId, int stock) {
String key = "seckill:stock:" + productId;
redisTemplate.opsForValue().set(key, stock);
}
}
8. 秒杀核心服务(SeckillService.java)- 重点:setNx 防重标记
package com.example.seckill.service;
import com.example.seckill.dao.OrderDao;
import com.example.seckill.entity.SeckillOrder;
import com.example.seckill.limiter.RedissonRateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
public class SeckillService {
private static final Logger logger = LoggerFactory.getLogger(SeckillService.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisStockManager stockManager;
@Autowired
private RedissonRateLimiter rateLimiter;
@Autowired
private OrderDao orderDao;
private ThreadPoolTaskExecutor asyncExecutor;
@PostConstruct
public void initExecutor() {
asyncExecutor = new ThreadPoolTaskExecutor();
asyncExecutor.setCorePoolSize(10);
asyncExecutor.setMaxPoolSize(50);
asyncExecutor.setQueueCapacity(200);
asyncExecutor.setThreadNamePrefix("order-async-");
asyncExecutor.initialize();
}
@PreDestroy
public void destroyExecutor() {
if (asyncExecutor != null) {
asyncExecutor.shutdown();
}
}
/**
* 秒杀下单入口
*/
public String seckill(Long userId, Long productId) {
// 1. 全局限流(每秒5000,桶容量8000)
if (!rateLimiter.tryAcquire("seckill:limiter:global", 5000, 8000)) {
return "系统繁忙,请稍后再试";
}
// 2. 商品限流(每秒1000,桶容量1500)
String productLimitKey = "seckill:limiter:product:" + productId;
if (!rateLimiter.tryAcquire(productLimitKey, 1000, 1500)) {
return "该商品太火爆,请稍后重试";
}
// 3. 重复下单检查(使用 Redis setNx + 3秒过期)⭐核心防重⭐
String repeatKey = "seckill:repeat:" + userId + ":" + productId;
// setIfAbsent 相当于 setNx,并同时设置过期时间(原子操作)
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(repeatKey, "1", Duration.ofSeconds(3));
if (Boolean.FALSE.equals(acquired)) {
// 3秒内重复请求,直接拦截
return "请勿重复下单";
}
// 4. 原子扣减Redis库存
String stockKey = "seckill:stock:" + productId;
boolean stockOk = stockManager.decrStock(stockKey, 1);
if (!stockOk) {
return "商品已售罄";
}
// 5. 扣减成功 -> 异步生成订单(落库)
asyncExecutor.submit(() -> {
try {
// 二次检查数据库是否已存在订单(兜底,防止极端情况)
if (orderDao.existsByUserIdAndProductId(userId, productId)) {
logger.warn("重复订单,userId={}, productId={},需补偿Redis库存", userId, productId);
// 此处可以触发库存回滚(实际生产中需要更可靠机制,这里仅演示)
return;
}
SeckillOrder order = new SeckillOrder();
order.setUserId(userId);
order.setProductId(productId);
order.setCreateTime(new Date());
orderDao.insertOrder(order);
logger.info("订单生成成功,userId={}, productId={}", userId, productId);
} catch (Exception e) {
logger.error("异步落库失败,userId={}, productId={}", userId, productId, e);
// 生产环境需要做补偿:重试或放入死信队列
}
});
return "秒杀成功,订单处理中";
}
/**
* 预热库存(供管理员调用)
*/
public void preloadStock(Long productId, int stock) {
stockManager.initStock(productId, stock);
logger.info("商品 {} 库存已预热,数量:{}", productId, stock);
}
}
9. Controller(SeckillController.java)
package com.example.seckill.controller;
import com.example.seckill.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SeckillController {
@Autowired
private SeckillService seckillService;
// 预热接口(实际由后台系统调用)
@PostMapping("/preload")
public String preload(@RequestParam Long productId, @RequestParam int stock) {
seckillService.preloadStock(productId, stock);
return "商品 " + productId + " 库存预热完成";
}
// 秒杀接口
@PostMapping("/seckill")
public String seckill(@RequestParam Long userId, @RequestParam Long productId) {
return seckillService.seckill(userId, productId);
}
}
10. 启动类(SeckillApplication.java)
package com.example.seckill;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SeckillApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class, args);
}
}
如何运行与测试
- 确保本地 Redis 已启动(默认 6379)。
- 运行
SeckillApplication。 - 预热库存:执行
POST http://localhost:8080/preload?productId=1001&stock=10。 - 模拟秒杀:用 JMeter 或 curl 多次请求
POST http://localhost:8080/seckill?userId=1&productId=1001。同一用户在 3 秒内重复请求会被setNx拦截,提示“请勿重复下单”。 - 3 秒后再次请求同一用户,可正常尝试(若库存还有剩余)。
关键点说明
- setNx 短时标记:
setIfAbsent(repeatKey, "1", Duration.ofSeconds(3))原子性完成“不存在则设置+过期时间”,3 秒内同一用户+商品只能通过一次请求。 - 为什么是 3 秒:覆盖正常网络往返时间 + 服务处理时间,同时允许用户因超时或网络问题在稍后重试,避免永久锁定。
- 数据库兜底:异步落库前依然检查
orderDao.existsByUserIdAndProductId,防止因标记失效或 Redis 故障导致重复订单。 - 限流与防重分工:限流保护系统不被流量冲垮,
setNx针对单个用户短时防重复点击,二者不矛盾且互补。
补充优化与注意事项
- 提前返回:库存扣减失败或重复下单等检查应尽早返回,不要在业务上锁。
- 热点数据:Redis 单点故障问题可使用 Redis Cluster 或 代理层;但秒杀场景下最好用主从 + 哨兵。
- 库存数据库同步:可以在活动结束后通过定时任务比对 Redis 和 DB 库存,或者每次秒杀成功异步扣减 DB 库存(用
乐观锁+version字段)。 - 客户端防重:前端点击秒杀后立即置灰,并配合后端重复标记。
- 缓存击穿:秒杀开始时所有请求可能打在一个 key 上,可以使用 本地缓存(Caffeine) 做一级过滤,但必须保证一致性不强求时可用。
- 订单查询:用户秒杀后要提供结果查询接口,从 Redis 或 DB 中读取生成状态。
更多推荐


所有评论(0)