1. 优惠券秒杀场景解析
电商大促期间最让技术团队头疼的莫过于秒杀场景。去年双十一我们团队负责的某品牌旗舰店上线了1000张限量优惠券,活动开始瞬间涌入50万请求,系统差点崩溃。这种高并发、强竞争的典型场景,正是Redis大显身手的舞台。
秒杀业务有三个核心特征:瞬时高并发(QPS通常破万)、库存精确控制(超卖直接导致资损)、极端响应速度(用户容忍度在毫秒级)。传统数据库方案在这类场景下会出现连接池耗尽、锁竞争激烈、磁盘IO瓶颈等问题。而Redis的单线程内存操作特性,配合原子命令和丰富的数据结构,能完美应对这些挑战。
2. Redis秒杀方案设计
2.1 核心架构设计
我们采用的方案是Redis+Lua脚本+异步落库的组合拳。具体流程是:
- 用户请求先经过Nginx限流
- 进入Redis进行库存校验和扣减
- 成功者进入消息队列异步创建订单
- 最终一致性更新数据库
这个架构中Redis承担了最关键的库存扣减职责,其内存操作和原子性保证了核心链路的高性能。实测在16核32G服务器上,Redis单节点可以稳定支撑3万+ QPS的秒杀请求。
2.2 数据结构选型
库存扣减需要同时满足原子性和高性能,我们对比了三种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| String+DECR | 最简单直观 | 无法防止超卖 |
| Hash+WATCH | 支持事务 | 性能较差(2000QPS) |
| Lua脚本+INCRBY | 原子性+高性能(3万QPS) | 开发复杂度略高 |
最终选择Lua脚本方案,核心逻辑如下:
lua复制local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
return 0
end
redis.call('DECR', KEYS[1])
return 1
2.3 防超卖设计
秒杀系统最怕的就是库存扣减出现竞态条件。我们通过多重防护确保万无一失:
- Lua脚本保证原子性操作
- 预扣减机制:活动前将库存从DB加载到Redis
- 双重校验:Redis扣减后,MQ消费者再次校验数据库
- 库存回滚:15分钟未支付自动释放库存
关键提示:永远不要在应用层做库存判断,必须依赖Redis的原子操作。我们曾因在Java代码中先查询后扣减,导致超卖200多单。
3. 性能优化实战
3.1 热点key处理
秒杀商品必然成为热点key,我们采用三种策略应对:
- key分片:coupon:stock:
- 本地缓存:客户端缓存库存状态5秒
- 读写分离:读请求走从节点
3.2 集群部署方案
单节点Redis再强也有性能上限。我们搭建了Redis Cluster方案:
- 6个主节点(16G内存)
- 每个主节点1个从节点
- 使用CRC16算法自动分片
- 客户端采用JedisCluster
实测该集群可支撑20万+ QPS,完全满足大促需求。配置关键参数:
bash复制cluster-enabled yes
cluster-node-timeout 15000
cluster-migration-barrier 1
3.3 持久化策略取舍
在秒杀场景下我们选择牺牲部分持久化保证性能:
- 关闭AOF持久化
- RDB配置为1小时1次
- 主从同步采用全量+增量
这是因为:
- 秒杀数据可通过DB重建
- 极端情况下丢失几分钟数据可接受
- 持久化会引发fork阻塞影响性能
4. 异常处理方案
4.1 缓存雪崩预防
所有秒杀系统必须防范雪崩效应,我们采用多层防护:
- 差异化过期:库存key设置随机过期时间(30分钟±300秒)
- 永不过期策略:通过后台线程定期更新
- 熔断降级:库存查询失败时直接返回已售罄
4.2 防刷机制实现
为了防止脚本抢券,我们增加了这些限制:
- 用户维度计数:incr user:limit:
- IP速率限制:redis.call('CL.THROTTLE', ipKey, 10, 60, 60)
- 行为验证:扣减前校验验证码
4.3 数据一致性保障
采用最终一致性方案处理Redis与DB的数据同步:
- 扣减成功消息进入RabbitMQ
- 消费者幂等处理订单创建
- 定时任务补偿异常订单
- 对账系统每小时全量校验
5. 监控与压测
5.1 关键监控指标
我们配置了这些监控看板:
- Redis性能:CPU、内存、网络IO
- 集群状态:节点角色、槽位分布
- 业务指标:剩余库存、抢购成功率
- 慢查询:超过10ms的命令
使用Prometheus+Grafana实现可视化,关键告警项:
yaml复制- alert: HighRedisCPU
expr: rate(redis_cpu_sys_seconds_total[1m]) > 0.8
for: 5m
5.2 全链路压测
在预发环境进行了多轮压测,主要发现:
- 连接池瓶颈:调整maxTotal=500
- 序列化性能:改用Kryo替代JDK序列化
- 网络延迟:启用TCP_NODELAY参数
压测工具采用JMeter,关键配置:
code复制线程组:5000线程
ramp-up:60秒
循环次数:永远
6. 完整实现示例
6.1 SpringBoot集成方案
核心服务层实现:
java复制@Service
public class CouponService {
private final RedisTemplate<String, String> redisTemplate;
public boolean seckillCoupon(Long couponId, Long userId) {
String script = "local stock = tonumber(redis.call('GET', KEYS[1]))...";
RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList("coupon:stock:" + couponId),
Collections.emptyList()
);
if (result == 1) {
mqTemplate.send("coupon_order",
new OrderMessage(couponId, userId));
return true;
}
return false;
}
}
6.2 消费者实现
订单创建消费者示例:
java复制@RabbitListener(queues = "coupon_order")
public void processOrder(OrderMessage message) {
// 再次校验库存
Integer stock = jdbcTemplate.queryForObject(
"SELECT stock FROM coupon WHERE id = ?",
Integer.class, message.getCouponId());
if (stock > 0) {
jdbcTemplate.update(
"UPDATE coupon SET stock = stock - 1 WHERE id = ?",
message.getCouponId());
orderMapper.insert(new Order(...));
} else {
// 补偿Redis库存
redisTemplate.opsForValue().increment(
"coupon:stock:" + message.getCouponId());
}
}
6.3 前端交互优化
为了提升用户体验,我们实现了这些优化:
- 静态化活动页:Nginx直接返回HTML
- 按钮状态控制:库存为0时立即禁用
- 请求合并:用户快速点击时合并请求
- 轮询结果:扣减成功后轮询订单状态
前端关键代码片段:
javascript复制async function handleSeckill() {
const { data } = await axios.post('/api/seckill', {
couponId: 123,
token: 'xxx'
}, {
timeout: 3000
});
if (data.success) {
startPollingOrderStatus();
} else {
showSoldOut();
}
}
7. 踩坑经验总结
在实际落地过程中,我们遇到过这些典型问题:
Jedis连接泄漏
现象:压测期间出现Cannot get Jedis connection
解决:确保每次Jedis.close()执行,改用try-with-resources
集群节点失效
现象:部分slot不可用导致请求失败
解决:设置cluster-require-full-coverage=no
Lua脚本超时
现象:执行超过5秒被中断
解决:优化脚本逻辑,避免大key操作
库存不同步
现象:Redis与DB库存出现偏差
解决:增加定时核对任务,凌晨修复差异
缓存穿透
现象:大量请求不存在的couponId
解决:布隆过滤器前置校验
这些经验让我深刻认识到,Redis秒杀方案虽然性能优异,但细节决定成败。特别是在大促场景下,任何小问题都会被无限放大。建议大家在开发完成后,至少进行三轮全链路压测:基础压测、故障注入压测、极限压测。