高并发秒杀系统实战:Redis+Lua+Redisson的架构设计与避坑指南
在电商大促期间,每秒数万次的抢购请求让传统系统架构不堪重负。我曾负责一个类似"黑马点评"的优惠券秒杀模块开发,经历了从数据库崩溃到稳定支撑十万级QPS的完整演进过程。本文将分享如何用Redis+Lua+Redisson构建高可靠秒杀系统,以及那些只有踩过坑才知道的实战经验。
1. 秒杀系统的核心挑战与设计原则
当用户疯狂点击"立即抢购"按钮时,系统实际上在经历一场没有硝烟的战争。我们首先需要明确秒杀场景下的三大核心问题:
- 超卖现象:100件库存卖出120单,这是最典型的并发问题
- 重复下单:用户连续点击导致生成多个订单
- 系统雪崩:数据库连接池被耗尽,整个服务不可用
性能基准测试数据对比:
| 方案 | 吞吐量(QPS) | 平均响应时间(ms) | 资源消耗 |
|---|---|---|---|
| 纯数据库方案 | 120 | 850 | 高 |
| Redis简单计数 | 2,800 | 35 | 中 |
| Lua+Redisson(最终方案) | 18,000 | 8 | 低 |
提示:实际压测需考虑网络延迟和业务逻辑复杂度,本数据基于4核8G云服务器测试环境
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis库存预热的正确姿势
很多团队知道要把库存加载到Redis,但往往忽略了关键细节。这是我们最初版本的库存初始化代码:
java复制// 错误示范:简单设置库存值
stringRedisTemplate.opsForValue().set("stock:"+voucherId, "100");
这种写法存在两个致命缺陷:
- 没有设置过期时间,可能导致冷数据长期占用内存
- 缺乏原子性保证,在集群环境下可能重复初始化
改进后的库存预热方案:
java复制// 使用Lua脚本保证原子性初始化
String script =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('set', KEYS[1], ARGV[1]) " +
" redis.call('pexpire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
// 设置24小时过期
stringRedisTemplate.execute(redisScript,
Collections.singletonList("stock:"+voucherId),
"100",
"86400000");
3. Lua脚本实现原子库存扣减
单纯使用Redis的DECR命令无法解决所有并发问题。这是我们趟过坑后设计的Lua脚本:
lua复制-- KEYS[1]: 库存key
-- KEYS[2]: 订单集合key
-- ARGV[1]: 用户ID
-- ARGV[2]: 当前时间戳(用于订单去重)
-- 检查库存
local stock = tonumber(redis.call('get', KEYS[1]))
if not stock or stock <= 0 then
return 1 -- 库存不足
end
-- 检查是否重复下单
local exists = redis.call('zscore', KEYS[2], ARGV[1])
if exists then
return 2 -- 已存在订单
end
-- 扣减库存
redis.call('decr', KEYS[1])
-- 记录订单(带时间戳用于后续排查)
redis.call('zadd', KEYS[2], ARGV[2], ARGV[1])
return 0
这个脚本解决了三个关键问题:
- 原子性执行所有操作
- 使用有序集合记录订单,便于后期分析
- 通过时间戳实现请求幂等
4. Redisson分布式锁的进阶用法
很多人知道用RedissonLock,但往往忽略这些优化点:
锁配置的最佳实践:
java复制RLock lock = redissonClient.getLock("order:lock:" + userId);
// 尝试获取锁,最多等待100ms,锁自动释放时间30秒
boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
if (!locked) {
throw new BusinessException("操作太频繁,请稍后再试");
}
try {
// 业务逻辑
} finally {
// 只释放自己持有的锁
if(lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
锁的注意事项:
- 避免设置过长的leaseTime,建议不超过30秒
- 必须检查锁持有状态再释放,防止误删他人锁
- 考虑实现锁续期机制处理长事务
5. 异步订单处理的架构设计
秒杀成功后的订单处理应该与抢购逻辑解耦。我们采用的方案是:
code复制[抢购核心流程]
↓
[Redis Stream队列]
↓
[消费者集群]
↓
[数据库分库]
生产者端代码示例:
java复制// 抢购成功后发送到消息队列
Map<String, String> message = new HashMap<>();
message.put("voucherId", voucherId.toString());
message.put("userId", userId.toString());
message.put("orderId", orderId.toString());
stringRedisTemplate.opsForStream().add("order:queue", message);
消费者端关键配置:
properties复制# 每个消费者线程处理的消息数
spring.redis.stream.consumer.batch-size=20
# 失败消息重试次数
spring.redis.stream.consumer.max-attempts=3
# 消费组名称
spring.redis.stream.consumer.group=order-group
6. 性能优化与异常处理
在百万级压力测试中,我们总结出这些经验:
缓存预热策略:
- 提前1小时加载秒杀数据到Redis
- 使用多线程并行加载不同商品数据
- 设置合理的过期时间避免内存泄漏
限流方案对比:
| 方案 | 实现复杂度 | 精准度 | 性能影响 |
|---|---|---|---|
| Redis计数器 | 低 | 中 | 小 |
| 令牌桶算法 | 中 | 高 | 中 |
| 漏桶算法 | 高 | 高 | 大 |
熔断降级配置示例:
java复制@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new CircuitBreakerConfig.Builder()
.failureRateThreshold(50) // 失败率阈值
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断时间
.slidingWindowSize(100) // 滑动窗口大小
.build());
}
7. 监控与排查技巧
没有完善的监控,秒杀系统就像盲人摸象。我们采用的监控方案:
-
Redis关键指标监控:
bash复制# 监控内存使用 redis-cli info memory # 监控命令统计 redis-cli info commandstats -
订单异常检测脚本:
python复制# 检查订单与库存的一致性 import redis r = redis.Redis() stock = r.get('stock:1001') orders = r.zcard('orders:1001') print(f"理论剩余库存: {int(stock)}") print(f"实际售出数量: {orders}") -
慢查询分析:
sql复制-- MySQL慢查询分析 SELECT * FROM mysql.slow_log WHERE start_time > NOW() - INTERVAL 1 HOUR ORDER BY query_time DESC LIMIT 10;
在实施这套方案后,我们的系统在618大促期间平稳支撑了峰值15万QPS的秒杀请求,库存准确率达到100%,订单处理延迟控制在200ms以内。记住,高并发系统没有银弹,只有不断调优和演练才能保证万无一失。
