SpringBoot + Redis + Lua:秒杀系统设计,超卖防护 + 库存预热 + 流量削峰全方案
引言:秒杀系统的挑战
双11、618等大促活动,用户疯狂点击购买按钮,结果出现超卖,库存变成负数?或者系统直接被高并发请求压垮,用户看到的都是错误页面?再或者大量的无效请求消耗了系统资源,真正想购买的用户反而买不到?
这就是秒杀系统的经典难题。传统的电商系统架构无法应对瞬间爆发的高并发请求。今天我们就来聊聊如何用SpringBoot + Redis + Lua构建一个高并发的秒杀系统,实现超卖防护、库存预热、流量削峰。
秒杀系统的痛点分析
1. 超卖问题
在高并发场景下,多个请求同时读取库存,判断库存充足,然后都执行减库存操作,导致库存变为负数。
2. 系统崩溃
大量并发请求直接打到数据库,数据库连接数耗尽,系统响应变慢甚至崩溃。
3. 流量不均
用户可能在活动开始瞬间集中访问,造成流量冲击。
4. 库存不一致
数据库和缓存数据不一致,导致数据错误。
技术选型:为什么选择这些技术?
Redis:高性能缓存与原子操作
Redis具有以下优势:
- 高性能:内存操作,响应快
- 原子性:保证并发安全
- 丰富的数据结构:支持字符串、哈希、列表等
- 分布式支持:支持集群部署
Lua:原子性脚本执行
Lua脚本的优势:
- 原子性:脚本执行期间其他命令无法执行
- 高效性:减少网络往返
- 灵活性:可以实现复杂逻辑
SpringBoot:快速开发与集成
SpringBoot提供了:
- 自动配置:快速集成Redis
- 注解支持:@Cacheable等便捷注解
- AOP支持:便于统一处理
系统架构设计
我们的秒杀系统主要包括以下几个模块:
- 库存预热:提前加载库存到Redis
- 流量控制:限制请求频率
- 库存扣减:原子性扣减库存
- 订单处理:异步处理订单
- 结果返回:快速返回结果
- 补偿机制:处理异常情况
核心实现思路
1. 库存预热
在秒杀活动开始前,将商品库存加载到Redis:
@Service
public class StockPreheatService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductRepository productRepository;
/**
* 预热库存
*/
public void preheatStock(Long productId) {
// 从数据库获取商品信息
Product product = productRepository.findById(productId);
// 将库存信息加载到Redis
String stockKey = "stock:" + productId;
redisTemplate.opsForValue().set(stockKey, product.getStock(), Duration.ofHours(2));
// 设置商品信息
String productKey = "product:" + productId;
redisTemplate.opsForValue().set(productKey, product, Duration.ofHours(2));
// 初始化秒杀状态
String seckillStatusKey = "seckill:status:" + productId;
redisTemplate.opsForValue().set(seckillStatusKey, "ready", Duration.ofHours(2));
}
/**
* 批量预热库存
*/
public void batchPreheatStock(List<Long> productIds) {
productIds.parallelStream().forEach(this::preheatStock);
}
}
2. Lua脚本实现原子性扣减
使用Lua脚本保证库存扣减的原子性:
@Component
public class SeckillLuaScript {
private static final String SECKILL_SCRIPT =
"local stockKey = KEYS[1]\n" +
"local orderId = ARGV[1]\n" +
"local userId = ARGV[2]\n" +
"\n" +
"-- 检查库存\n" +
"local stock = redis.call('GET', stockKey)\n" +
"if not stock then\n" +
" return {code = 1, message = '库存不存在'}\n" +
"end\n" +
"\n" +
"stock = tonumber(stock)\n" +
"if stock <= 0 then\n" +
" return {code = 2, message = '库存不足'}\n" +
"end\n" +
"\n" +
"-- 扣减库存\n" +
"redis.call('DECR', stockKey)\n" +
"\n" +
"-- 记录用户秒杀记录,防止重复秒杀\n" +
"local userKey = 'seckill:user:' .. ARGV[2] .. ':' .. KEYS[1]\n" +
"local exists = redis.call('GET', userKey)\n" +
"if exists then\n" +
" -- 库存回滚\n" +
" redis.call('INCR', stockKey)\n" +
" return {code = 3, message = '您已参与过本次秒杀'}\n" +
"end\n" +
"\n" +
"-- 记录用户秒杀记录\n" +
"redis.call('SET', userKey, '1', 'EX', 86400) -- 24小时过期\n" +
"\n" +
"return {code = 0, message = '秒杀成功', stock = stock - 1}";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 执行秒杀
*/
public SeckillResult executeSeckill(Long productId, Long userId) {
String stockKey = "stock:" + productId;
String[] keys = new String[]{stockKey};
String[] args = new String[]{String.valueOf(System.currentTimeMillis()), String.valueOf(userId)};
try {
RedisScript<String> script = new DefaultRedisScript<>(SECKILL_SCRIPT, String.class);
String result = (String) redisTemplate.execute(script, Arrays.asList(keys), args);
// 解析结果
return parseResult(result);
} catch (Exception e) {
log.error("秒杀执行失败", e);
return SeckillResult.failure("系统异常,请稍后再试");
}
}
private SeckillResult parseResult(String result) {
// 解析Lua脚本返回的结果
// 实际项目中需要根据具体格式解析
return JSON.parseObject(result, SeckillResult.class);
}
}
3. 流量削峰
使用令牌桶算法控制请求流量:
@Component
public class TrafficShapingService {
private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
/**
* 获取令牌
*/
public boolean tryAcquire(String key, int permits) {
RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(key,
k -> RateLimiter.create(100)); // 每秒100个令牌
return rateLimiter.tryAcquire(permits);
}
/**
* 动态调整限流参数
*/
public void updateRate(String key, double permitsPerSecond) {
RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(key,
k -> RateLimiter.create(permitsPerSecond));
rateLimiter.setRate(permitsPerSecond);
}
}
4. 秒杀服务
@Service
public class SeckillService {
@Autowired
private SeckillLuaScript seckillLuaScript;
@Autowired
private TrafficShapingService trafficShapingService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderProducer orderProducer; // 消息队列生产者
public SeckillResult seckill(Long productId, Long userId) {
// 1. 流量控制
if (!trafficShapingService.tryAcquire("seckill:" + productId, 1)) {
return SeckillResult.failure("当前参与人数过多,请稍后再试");
}
// 2. 检查秒杀活动状态
String seckillStatusKey = "seckill:status:" + productId;
String status = (String) redisTemplate.opsForValue().get(seckillStatusKey);
if (!"active".equals(status)) {
return SeckillResult.failure("秒杀活动未开始或已结束");
}
// 3. 执行秒杀(原子性操作)
SeckillResult result = seckillLuaScript.executeSeckill(productId, userId);
if (result.isSuccess()) {
// 4. 异步创建订单
OrderMessage orderMessage = new OrderMessage(productId, userId, result.getOrderId());
orderProducer.sendOrderMessage(orderMessage);
}
return result;
}
}
5. 消息队列处理订单
@Component
public class OrderConsumer {
@Autowired
private OrderService orderService;
@RabbitListener(queues = "seckill.order.queue")
public void processOrder(OrderMessage message, Channel channel, @Header Map<String, Object> headers) {
try {
// 创建订单
Order order = new Order();
order.setProductId(message.getProductId());
order.setUserId(message.getUserId());
order.setOrderTime(new Date());
order.setStatus(OrderStatus.PENDING);
orderService.createOrder(order);
// 确认消息
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("处理订单失败", e);
try {
// 拒绝消息,重新入队
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicNack(deliveryTag, false, true);
} catch (IOException ioException) {
log.error("拒绝消息失败", ioException);
}
}
}
}
高级特性实现
1. 库存分片
@Component
public class ShardedStockService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 分片库存扣减
*/
public boolean deductStock(Long productId, int quantity) {
String baseKey = "stock:" + productId;
int shardCount = 10; // 分片数量
// 计算需要扣减的分片
int remaining = quantity;
for (int i = 0; i < shardCount && remaining > 0; i++) {
String shardKey = baseKey + ":shard:" + i;
Long currentStock = redisTemplate.opsForValue().increment(shardKey, -remaining);
if (currentStock < 0) {
// 回滚已扣减的库存
redisTemplate.opsForValue().increment(shardKey, remaining + currentStock);
remaining = -currentStock.intValue();
} else {
remaining = 0;
}
}
return remaining == 0;
}
}
2. 用户限流
@Component
public class UserRateLimitService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 用户请求限流
*/
public boolean isAllowed(Long userId, String action, int limit, int windowSeconds) {
String key = "rate_limit:" + userId + ":" + action;
String luaScript =
"local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local window = tonumber(ARGV[2])\n" +
"local current = redis.call('GET', key)\n" +
"if current == false then\n" +
" redis.call('SET', key, 1)\n" +
" redis.call('EXPIRE', key, window)\n" +
" return 1\n" +
"end\n" +
"current = tonumber(current)\n" +
"if current < limit then\n" +
" redis.call('INCR', key)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = (Long) redisTemplate.execute(script, Arrays.asList(key),
String.valueOf(limit), String.valueOf(windowSeconds));
return result == 1;
}
}
3. 预售模式
@Service
public class PreSaleService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 预售秒杀
*/
public SeckillResult preSaleSeckill(Long productId, Long userId, String deposit) {
String preSaleKey = "presale:" + productId;
String depositKey = "deposit:" + productId + ":" + userId;
// 检查是否已支付定金
String userDeposit = (String) redisTemplate.opsForValue().get(depositKey);
if (!deposit.equals(userDeposit)) {
return SeckillResult.failure("请先支付定金");
}
// 执行秒杀逻辑
return executeSeckill(productId, userId);
}
}
性能优化建议
1. 连接池优化
spring:
redis:
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10
max-wait: 2000ms
timeout: 1000ms
2. 批量操作
public class BatchOperationService {
public void batchUpdateStock(List<StockUpdate> updates) {
String luaScript =
"for i = 1, #KEYS, 1 do\n" +
" redis.call('DECRBY', KEYS[i], ARGV[i])\n" +
"end\n" +
"return 'OK'";
List<String> keys = updates.stream().map(u -> "stock:" + u.getProductId()).collect(Collectors.toList());
List<String> args = updates.stream().map(u -> String.valueOf(u.getQuantity())).collect(Collectors.toList());
redisTemplate.execute(new DefaultRedisScript<>(luaScript, String.class), keys, args.toArray(new String[0]));
}
}
安全考虑
1. 防刷机制
@Component
public class AntiBrushService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean isBrushing(String ip, Long userId) {
String ipKey = "brush:ip:" + ip;
String userKey = "brush:user:" + userId;
// 检查IP请求频率
Long ipCount = redisTemplate.opsForValue().increment(ipKey);
if (ipCount > 10) { // 10秒内超过10次请求
redisTemplate.expire(ipKey, Duration.ofSeconds(10));
return true;
}
// 检查用户请求频率
Long userCount = redisTemplate.opsForValue().increment(userKey);
if (userCount > 5) { // 10秒内超过5次请求
redisTemplate.expire(userKey, Duration.ofSeconds(10));
return true;
}
// 设置过期时间
redisTemplate.expire(ipKey, Duration.ofSeconds(10));
redisTemplate.expire(userKey, Duration.ofSeconds(10));
return false;
}
}
2. 接口防重
@RestController
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private AntiBrushService antiBrushService;
@PostMapping("/seckill/{productId}")
public ResponseEntity<SeckillResult> seckill(
@PathVariable Long productId,
@RequestParam Long userId,
@RequestHeader("X-Real-IP") String ip,
@RequestParam String requestId) { // 防重标识
// 防刷检查
if (antiBrushService.isBrushing(ip, userId)) {
return ResponseEntity.ok(SeckillResult.failure("请求过于频繁"));
}
// 防重检查
String requestKey = "request:" + requestId;
if (redisTemplate.hasKey(requestKey)) {
return ResponseEntity.ok(SeckillResult.failure("请勿重复提交"));
}
// 设置请求标识,防止重复
redisTemplate.opsForValue().set(requestKey, "1", Duration.ofMinutes(5));
SeckillResult result = seckillService.seckill(productId, userId);
return ResponseEntity.ok(result);
}
}
监控与告警
1. 关键指标监控
@Component
public class SeckillMetricsCollector {
private final MeterRegistry meterRegistry;
public void recordSeckillAttempt(Long productId, boolean success) {
Counter.builder("seckill_attempts_total")
.tag("product_id", productId.toString())
.tag("result", success ? "success" : "failure")
.register(meterRegistry)
.increment();
}
public void recordStock(Long productId, int stock) {
Gauge.builder("seckill_stock")
.tag("product_id", productId.toString())
.register(meterRegistry, stock, s -> (double) s);
}
}
2. 异常告警
@Component
public class SeckillAlertService {
public void checkStockAlert(Long productId, int currentStock) {
if (currentStock < 100) { // 库存不足告警
alertService.sendAlert("库存不足告警",
String.format("商品[%d]库存不足,当前库存: %d", productId, currentStock));
}
}
}
最佳实践
1. 分阶段限流
public class PhasedRateLimitService {
public double getRateLimit(String phase) {
switch (phase) {
case "before":
return 10; // 活动前:10 QPS
case "start":
return 1000; // 活动开始:1000 QPS
case "peak":
return 5000; // 高峰期:5000 QPS
case "end":
return 100; // 活动结束:100 QPS
default:
return 100;
}
}
}
2. 熔断降级
@Service
public class SeckillCircuitBreaker {
private final CircuitBreaker circuitBreaker;
public SeckillResult seckillWithCircuitBreaker(Long productId, Long userId) {
return circuitBreaker.executeSupplier(() -> seckillService.seckill(productId, userId));
}
}
总结
通过SpringBoot + Redis + Lua的组合,我们可以构建一个高并发的秒杀系统。关键在于:
- 库存预热:提前加载库存到Redis
- 原子操作:使用Lua脚本保证扣库存原子性
- 流量控制:通过限流算法削峰填谷
- 异步处理:订单处理异步化
- 安全防护:防刷、防重、防超卖
记住,秒杀系统不是一蹴而就的,需要根据实际业务场景持续优化。掌握了这些技巧,你就能构建一个稳定高效的秒杀系统,告别超卖和系统崩溃的烦恼。
标题:SpringBoot + Redis + Lua:秒杀系统设计,超卖防护 + 库存预热 + 流量削峰全方案
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/03/1767450416770.html
- 引言:秒杀系统的挑战
- 秒杀系统的痛点分析
- 1. 超卖问题
- 2. 系统崩溃
- 3. 流量不均
- 4. 库存不一致
- 技术选型:为什么选择这些技术?
- Redis:高性能缓存与原子操作
- Lua:原子性脚本执行
- SpringBoot:快速开发与集成
- 系统架构设计
- 核心实现思路
- 1. 库存预热
- 2. Lua脚本实现原子性扣减
- 3. 流量削峰
- 4. 秒杀服务
- 5. 消息队列处理订单
- 高级特性实现
- 1. 库存分片
- 2. 用户限流
- 3. 预售模式
- 性能优化建议
- 1. 连接池优化
- 2. 批量操作
- 安全考虑
- 1. 防刷机制
- 2. 接口防重
- 监控与告警
- 1. 关键指标监控
- 2. 异常告警
- 最佳实践
- 1. 分阶段限流
- 2. 熔断降级
- 总结
0 评论