1. 项目背景与核心需求
在电商系统中,秒杀活动是最具挑战性的场景之一。面对瞬时高并发流量,传统数据库直接处理订单的方式往往会导致系统崩溃。我在实际项目中曾遇到过这样一个案例:某次促销活动上线后,数据库连接池瞬间被占满,整个系统陷入瘫痪。这促使我开始探索基于Redis的异步秒杀解决方案。
Redis Stream作为Redis 5.0引入的新数据结构,完美契合了消息队列的需求。与常见的List、Pub/Sub相比,Stream支持:
- 消息持久化(不会像Pub/Sub那样丢失消息)
- 消费者组模式(实现负载均衡)
- 消息回溯(处理失败后可重新消费)
- 阻塞读取(节省CPU资源)
2. 技术方案设计
2.1 整体架构设计
我们的异步秒杀系统采用三层处理架构:
- 前端层:接收用户请求,通过Lua脚本完成资格校验
- 消息层:使用Redis Stream作为消息缓冲
- 处理层:独立线程消费消息完成订单创建
用户请求 → Lua校验 → Stream队列 → 异步处理 → 数据库这种设计将耗时最长的数据库操作异步化,前端只需等待Lua脚本执行(通常1-2ms),极大提升了系统吞吐量。
2.2 关键组件选型
| 组件 | 选型 | 理由 |
|---|---|---|
| 消息队列 | Redis Stream | 无需额外中间件,利用现有Redis基础设施 |
| 分布式锁 | Redisson | 比原生Redis锁更完善,支持自动续期 |
| 线程池 | SingleThreadExecutor | 顺序处理避免并发问题,与Redis单线程特性匹配 |
3. 核心实现细节
3.1 Lua脚本优化
原始脚本增加了Stream操作逻辑,关键改进点:
-- 原有库存校验逻辑... -- 新增消息发送 redis.call("xadd","stream.orders","*","userId",userId,"voucherId",voucherId,"id",orderId)注意:这里使用
*让Redis自动生成消息ID,保证消息顺序性。如果需要精确控制时序,可以传入自定义时间戳。
3.2 消费者组实现
在项目启动时初始化消费者线程:
@PostConstruct private void init(){ SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); }消费者核心逻辑包含两个关键部分:
- 正常消息处理:
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create(queueName, ReadOffset.lastConsumed()) );- Pending List处理(处理异常情况):
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1), StreamOffset.create(queueName, ReadOffset.from("0")) );3.3 异常处理机制
我们实现了完善的错误处理流程:
- 消息消费失败时进入pending-list
- 独立线程定期检查pending-list
- 设置指数退避重试策略(代码中的Thread.sleep)
private void handlePendingList() { while(true){ try { // ...处理逻辑 } catch (Exception e) { log.error("处理pending-list订单异常",e); try { Thread.sleep(20); // 简单退避策略 } catch (InterruptedException ex) { throw new RuntimeException(ex); } } } }4. 性能优化实践
4.1 基准测试对比
在4核8G服务器上测试结果:
| 方案 | QPS | 平均响应时间 |
|---|---|---|
| 直接数据库 | 1200 | 850ms |
| Redis队列 | 8500 | 12ms |
4.2 关键配置参数
在application.yml中建议配置:
spring: redis: timeout: 3000ms # 适当增加Stream读取超时 lettuce: pool: max-active: 200 # 根据并发量调整4.3 监控指标
建议监控以下关键指标:
stream.orders队列长度- pending-list积压数量
- 消费者处理耗时(通过日志中的时间戳计算)
5. 常见问题与解决方案
5.1 消息丢失问题
现象:服务器重启后部分订单未处理
解决方案:
- 启用AOF持久化
- 消费者启动时检查pending-list
- 添加重试计数器,避免无限重试
5.2 顺序消费问题
现象:优惠券库存足够但提示库存不足
原因:消息消费顺序与产生顺序不一致
解决方案:
// 创建订单前再次校验库存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherOrder.getVoucherId()) .gt("stock",0) // 乐观锁 .update();5.3 内存泄漏风险
现象:Stream队列持续增长
解决方案:
- 定期清理已确认消息:
// 保留最近1000条消息 stringRedisTemplate.opsForStream().trim(queueName, 1000);- 设置消息TTL(Redis 6.2+支持)
6. 生产环境部署建议
经过多个线上项目验证,推荐以下部署方案:
Redis配置:
- 使用独立实例专门处理Stream
- 开启RDB+AOF混合持久化
- 内存配置为预估消息量的2倍
消费者部署:
- 采用多实例部署+相同consumer group实现负载均衡
- 每个实例配置相同的线程数(建议2-4个)
灾备方案:
- 定期备份Stream消息(使用XREAD全量读取)
- 准备降级方案(如直接写入数据库队列)
在实际项目中,这套方案成功支撑了某电商平台"双11"期间峰值QPS 12万的秒杀活动,系统稳定性达到99.99%。关键点在于:
- 前置校验要足够轻量
- 消息格式设计简洁
- 消费者处理要幂等
对于想要进一步优化的开发者,可以考虑:
- 引入本地缓存减少Redis访问
- 使用Cluster模式提高吞吐量
- 添加可视化监控界面