1. 秒杀系统核心挑战与技术选型
秒杀场景是电商系统中典型的极端高并发场景,其核心特征表现为瞬时流量爆发(通常达到日常流量的10-100倍)、资源竞争激烈(库存有限)、操作耗时短(用户期望毫秒级响应)。在黑马点评这类本地生活平台中,秒杀券抢购业务面临三大技术挑战:
- 超卖问题:当库存仅剩1件时,1000个并发请求同时判断"有库存"导致超发
- 系统过载:瞬时DB查询压力导致连接池耗尽,正常业务受影响
- 恶意请求:脚本刷单导致真实用户无法公平参与
我们采用Redis+Lua+Stream的组合方案,主要基于以下考量:
- Redis:单线程内存操作避免锁竞争,10W+ QPS支撑高并发
- Lua脚本:原子性执行库存扣减与订单创建,彻底解决超卖
- Stream:异步处理订单持久化,削峰填谷保护下游数据库
关键设计原则:将同步操作压缩到最小(仅库存扣减),其他流程异步化处理。实测中该方案在4核8G服务器上可支撑2万+/秒的秒杀请求,库存准确率100%。
2. 核心架构分层实现
2.1 流量接入层设计
采用Nginx进行流量调度,关键配置参数:
limit_req_zone $binary_remote_addr zone=seckill:10m rate=100r/s; location /seckill { limit_req zone=seckill burst=200 nodelay; proxy_pass http://backend; }这实现了:
- 单个IP限流100请求/秒
- 允许200个请求的突发流量
- 直接拒绝超额请求(nodelay)
2.2 库存预热与原子扣减
使用Redis Hash结构存储商品信息:
HSET seckill:goods:1001 stock 100 version 1Lua脚本实现原子操作(示例核心逻辑):
local stock = tonumber(redis.call('HGET', goodsKey, 'stock')) if stock <= 0 then return 0 end local newStock = stock - 1 redis.call('HSET', goodsKey, 'stock', newStock) return newStock2.3 订单处理流水线
成功扣减库存后,将订单信息写入Redis Stream:
XADD orders * userId 10086 goodsId 1001 buyTime 1630000000独立消费者服务处理:
while(true) { List<MapRecord<String,String,String>> records = redisTemplate.opsForStream() .read(Consumer.from("group1", "consumer1"), StreamReadOptions.empty().count(100), StreamOffset.create("orders", ReadOffset.lastConsumed())); // 批量插入数据库 batchInsert(records); }3. 关键实现细节与优化
3.1 库存扣减的版本控制
采用乐观锁机制防止并发修改:
local version = tonumber(redis.call('HGET', goodsKey, 'version')) if version ~= tonumber(ARGV[1]) then return -1 end redis.call('HINCRBY', goodsKey, 'version', 1) redis.call('HSET', goodsKey, 'stock', newStock)客户端需先获取当前版本号,操作失败时重试3次。
3.2 分布式锁防重复提交
使用Redis实现令牌桶:
public boolean acquireToken(String userId) { String key = "seckill:token:" + userId; long now = System.currentTimeMillis(); // 每个用户5秒内只能获得1个令牌 return redisTemplate.opsForValue() .setIfAbsent(key, "1", 5, TimeUnit.SECONDS); }3.3 热点数据缓存策略
商品信息采用多级缓存:
- Nginx Proxy Cache缓存静态页面
- Redis缓存商品详情(设置随机过期时间防雪崩)
- 本地缓存(Caffeine)存储库存状态
4. 性能压测与调优记录
使用JMeter进行基准测试,关键指标优化过程:
| 优化阶段 | QPS | 平均响应时间 | 错误率 |
|---|---|---|---|
| 初始版本 | 1,200 | 850ms | 12% |
| 引入Redis | 8,000 | 120ms | 0.5% |
| 增加Lua脚本 | 15,000 | 65ms | 0% |
| 引入Stream异步 | 22,000 | 45ms | 0% |
关键调优参数:
# Redis连接池配置 spring.redis.lettuce.pool.max-active=500 spring.redis.lettuce.pool.max-wait=100ms # Stream消费者线程数 spring.task.execution.pool.core-size=165. 典型问题排查实录
5.1 Redis连接池耗尽
现象:出现"Could not get Redis connection"错误 解决方案:
- 增加连接池大小(max-active=500)
- 添加连接有效性测试(test-on-borrow=true)
- 设置合理的等待超时(max-wait=100ms)
5.2 订单重复消费
问题:网络抖动导致ACK失败,消息被重复投递 处理方案:
@Transactional public void processOrder(Order order) { if(orderMapper.selectById(order.getId()) != null) { return; // 幂等处理 } orderMapper.insert(order); }5.3 库存不一致
排查发现是缓存与数据库不同步导致:
- 增加双重校验:Redis扣减后记录日志
- 定时任务每小时校对库存
- 引入分布式事务消息(RocketMQ)确保最终一致
6. 生产环境部署建议
- Redis集群:采用3主3从架构,每个分片10G内存
- 监控体系:
- Prometheus采集Redis指标(内存、命中率、慢查询)
- Grafana设置QPS、库存变化监控面板
- 熔断降级:
@HystrixCommand(fallbackMethod = "fallbackSeckill") public Result seckill(Long goodsId) { // 核心逻辑 } - 数据预热:活动前1小时加载商品数据到Redis
这套方案在多个线上项目稳定运行,特别是在2023年双11期间,某电商平台峰值QPS达到35万,系统保持平稳。核心经验是:将同步操作最小化、用内存计算替代磁盘IO、通过异步化分解压力。后续可考虑引入RSocket进一步降低网络开销,或使用RedisTimeSeries实现实时风控。