我们做秒杀这个功能,主要的基本要求有两个:
- 第一个就是不能发生超卖、少卖可以接受
- 第二个就是需要保障用户体验,不能把系统给打挂了。
那其实总结一下秒杀其实只要做好一件事,那就是库存能扛得住热点扣减,并且不发生少卖就行了。
秒杀的核心 : 解决高并发
使用的核心组件 : Redis
, RocketMQ
,Mysql
以下是重要模块的设计
1. 前端模块
活动页面是用户流量的第一入口,所以是并发量最大的地方。如果这些流量都能直接访问服务端,恐怕服务端会因为承受不住这么大的压力,而直接挂掉。
- 前端方面采取静态页面 , 用 cdn 和浏览器进行缓存
- 在秒杀开始时 , 生成一个新的js文件 , 让 CDN中获取最新的js代码。
2. 拦截与限流
面对高并发请求 , 遵循 将请求尽量拦截在系统上游
的策略
需要考虑以下问题
2.1. 恶意的请求攻击防护
解决办法:
- 布隆过滤器(Bloom Filter) 构建有效商品ID白名单验证机制
- Redis空对象缓存(Null Object Pattern)配合MySQL回写兜底策略
原因分析:
- 布隆过滤器通过位图压缩存储结构(空间复杂度O(1)),可拦截99.9%的非法商品ID查询请求
- 空值缓存策略 采用TTL=60s的短期存储,避免缓存穿透导致数据库雪崩,配合互斥锁(Mutex Lock)实现单线程回写
2.2. 恶意的刷单脚本
解决办法:
- Nginx漏桶算法实现IP维度请求速率控制(rate=100r/s)
- 分布式令牌桶算法(Token Bucket)实施用户ID维度精准流控
备用策略:
- 动态验证码(滑块验证码),在流量异常时自动触发
原因分析:
- IP层限流可防御DDoS攻击,基于$binary_remote_addr变量实现滑动时间窗口计数
- 用户层令牌桶通过Redis+Lua脚本保证原子性递减,支持突发流量平滑处理
- 验证码兜底通过人机识别增加自动化脚本攻击成本(CAPTCHA破解耗时>500ms)
2.3. 业务层面的订单防重和接口幂等
问题 : 在很多秒杀场景中,用户为了能下单成功,会频繁的点击下单按钮,这时候如果没有做好控制的话,就可能会给一个用户创建重复订单 , 同时 , 我们还要保证 MQ 等重试机制的幂等防重
解决方案 :
我们设计使用 token 唯一令牌作为用户秒杀操作的唯一标识 , 贯穿整个过程 , 以保证接口幂等 , MQ 消费者幂等 , 支付幂等及对账标识
3. 处理高并发
面对秒杀这种高并发场景 , 我们需要精密设计各个环节以保证可靠性和高效性
3.1. 服务模块的设计
对秒杀流程分析可知 , 这是一个典型的读多写少
的场景 , 真正的高并发都存在于秒杀环节 , 大量用户抢少量商品,只有极少部分用户能够抢成功进入后面的订单模块和支付模块
所以我们便有一个设计思路 : 将秒杀功能作为一个微服务模块 , 与其他模块解耦 , 一方面我们可以对该高并发服务进行弹性扩缩容 , 另一方面 , 该高风险模块不会影响别的模块的功能
3.2. 缓存方案的设计
在秒杀场景下,读多写少 的特性使得 MySQL 难以承受高并发请求 ,因此,我们采用 Redis Cluster 作为核心缓存方案来对接高并发请求,以提升系统的并发处理能力并降低数据库压力。
3.3. 中间件的设计
在秒杀场景中,高并发流量容易对数据库和业务服务造成巨大冲击,因此,我们需要引入中间件来实现 流量消峰、异步解耦、事务消息 ,以保证系统的稳定性和高效处理能力。
4. 处理库存问题
4.1. 超卖问题
超卖是秒杀场景下的常见问题,由于商品库存有限,我们必须严格保证买与卖的数据一致性,防止并发下库存被重复扣减。
4.1.1. 方案一 : 分布式锁
解决方案 : 使用 Redis 的 SET NX + EX 实现互斥锁,在扣减库存前先获取锁,处理完毕后再释放锁,从而保证库存操作的原子性。
4.1.2. 方案二 : lua 脚本保证原子性( 推荐 )
我们最选择Redis 的 Lua 脚本 来实现库存扣减逻辑。Lua 脚本在 Redis 端原子执行,避免了分布式锁带来的额外开销,同时保证库存检查与扣减操作的一致性,有效防止超卖。
4.2. 预扣库存
我们的库存扣减这里,没有采用传统的下单扣库存或者支付扣库存,而是结合了这两者。
即下单的时候做预扣减,付款成功后做真正的扣减。好处是不仅可以减少超卖的风险,还能提升并发量。
预扣减通过 Redis 做一些流量的过滤,利用他的单线程、高性能机制,限制只有一部分用户可以"下单成功",其他用户则被拦住。因为 Redis 不可靠,所以这部分"下单成功"的用户需要在数据库中也进行一次库存扣减。
5. 数据一致性问题
这里的数据一致性问题主要是 mysql
和 redis
的数据一致性
但是这里的数据一致性问题跟一般的基于 mysql 为权威操作的情况不一样 ,我们的电商秒杀是几乎完全依赖于 redis 与用户的秒杀对接 , 在这种场景下 , 只是通知 mysql 数据的变动与修改 , 以保证后面的降级策略.
我们采取的策略是 Redis
异步通知 Msql
, 并通过定时任务进行补偿机制校验 , 还需要记录 Redis 的操作日志 , 以便后面人工介入处理作为最终的保障
6. 操作失败问题
场景分析
- 生产者发送给 mq 失败
解决 : ack 机制 + job 重试机制
- mq 消费信息失败
解决 : 业务幂等 和消息去重
- 创建订单失败
解决 : mq 的事务消息 + 重试机制
7. 降级策略
我们并不能把所有高并发请求完全依赖于 REDIS
和 RocketMQ
降级策略:
- 当Redis集群不可用时,启用MySQL行锁(如
SELECT ... FOR UPDATE
)处理请求,牺牲性能保业务 - 启动严格限流策略 ,随机策略对比例的用户直接返回秒杀失败,以降低 MySQL 的并发量
8. 关单策略
在秒杀场景中,用户下单后未及时支付的订单需要在指定时间后自动关闭,以释放库存资源。传统的关单方案通常采用基于 RocketMQ 的定时任务机制。
8.1. 传统方案:
- 订单创建:用户成功下单后,系统生成订单数据并存储。
- 发送延迟消息:系统通过 RocketMQ 发送一条延迟消息,设定延迟时间为订单的支付时限(例如 15 分钟)。
- 消息到期处理:当延迟时间到达,RocketMQ 将消息投递给消费者,消费者执行关单操作,检查订单支付状态,若未支付则关闭订单并释放库存。
缺点:
- 可靠性问题:虽然消息队列一般来说可靠性较高,但是也没办法做到100%不丢消息,所以在极端情况下,会有丢消息的风险。
- 资源浪费:如果系统中存在大量订单,为每一个订单都创建一个延迟消息可能会导致消息队列中积压大量的消息,这不仅增加了消息队列的资源消耗,也可能导致增加成本
- 大量无效消息(重要) :使用MQ实现订单到期关闭就要把订单放到MQ中,但是大部分订单会提前取消或者完成支付,这就会导致很多无效的消息。
8.2. 基于 主动 + 被动组合的方式的关单策略 (推荐)
订单创建成功之后,超过一定时间不支付,就需要关闭订单,这里方案有很多,比如用 MQ 、定时任务、Redis 等等。
8.2.1. 主动关单
我们使用定时任务对订单进行批扫描关单
这里在关单时做一次判断,查询支付单,判断是否有已经支付过的,避免渠道支付回调已经回来了,但是还没来得及通知订单模块,导致误关单的问题。
8.2.2. 被动关单
所谓被动关单,就是说当用户想要操作这个订单的时候,我们去检查下是不是已经超时了,如果超时了,并且状态还不是已关闭的话,那就执行一次关单操作。
这种被动关单,我们在两个地方都加了:
- 1、用户对订单支付时
- 2、用户查看订单详情时
8.2.3. 回滚策略
基于RocketMQ事务消息实现订单取消的一致性
在定时任务扫描出超时订单时 , 我们通过 RocketMq
的事务消息来保证 回滚 redis , 取消订单与回滚数据库的一致性
9. 实现流程
- 流量进入后端进行nginx , 令桶牌 , 布隆过滤器进行风控限流
- 组装订单和用户信息生成秒杀预订单id , 使用RocketMq发送半消息 , 进行本地事务操作 , lua 脚本扣减库存和新增扣减流水
- redis 预扣减后 , 向前端返回订单号后 , 提交 commit 半消息
- 订单模块异步消费信息 , 进行数据库预扣减 , 生成数据库扣减流水
- 最后创建订单返回给前端
- 启动定时任务扫表对账和进行订单超时处理