一、分析秒杀系统
1、秒杀系统的特点
- 高并发:瞬间流量大
- 超卖问题:秒杀商品库存少
- 接口防刷:秒杀前刷新频繁
2、秒杀系统需要考虑的点
秒杀前:
- 页面资源访问多:资源静态化,存CDN就近访问,开启资源压缩,减少传输数据量,提高速度
- 秒杀按钮置灰:前端可通过秒杀按钮置灰来拦截部分流量到后端,如秒杀前置灰,请求未返回置灰
- 秒杀真链接隐藏:秒杀开始后才暴露秒杀真链接,防止刷子提前获取链接做脚本刷新
秒杀中:
- 高并发/快响应:上线前要做好压测,评估服务器qps瓶颈点
- 防止超卖:大量用户抢少量商品,需要防止高并发(可以用redis的lua脚本保证原子性操作)
- 限流:限制用户请求频率,如一个人1s内只能请求10次,一个ip最多请求10次等
秒杀后:
- 异步处理订单后续:秒杀成功后,同步返回秒杀结果,并异步处理订单后续(如生成订单、通知等)
- 服务降级(系统):如用户秒杀成功但没正常生成订单,需要用MQ处理并重试,并且需要做好监控告警
- 服务降级(人工):如商品错误设置了价格(如iphone设置了1元一台),为了及时止损,需要加一个降级开关,返回秒杀已结束等文案
二、设计秒杀系统
1、流程
- 将商品(如库存等)提前写入到缓存里
- 用户秒杀后,我们要在redis对商品进行扣减
- 如果秒杀成功,则把秒杀成功的消息发送到mq,具体的下游业务异步处理(如订单服务、支付服务等)
- 同步更新MySQL数据(如库存等)
2、如何实现高并发?
背景:秒杀瞬时流量大,所以如何防止高并发造成缓存击穿或者缓存失效、击垮数据库都是我们需要考虑的问题
方案:
- 使用redis集群代替单体redis:考虑到缓存击穿问题,我们可以构建redis集群(如哨兵模式),来保证系统高可用
- …
3、如何实现接口防刷/限流?
方案:
- 前端限流:多次点击秒杀按钮之间,可以将按钮置灰n秒,也就是无法连续点击
- 后端限流:滑动窗口算法限流(用户uid作为key统计次数)、令牌桶算法限流(有效应对瞬时流量)
4、如何实现秒杀链接的动态性?
方案:可以在URL后面拼接一串MD5加密后的随机字符串,具体步骤为前端访问后端获取秒杀的URL,然后再访问该URL,后端会对该URL进行校验,校验通过的画才能继续秒杀操作
5、数据库设计
背景:防止秒杀系统崩了影响其他业务,我们的秒杀系统数据库一般不能与其他业务数据库放在一起
可以设计的秒杀系统表有:
- 秒杀商品表
- 秒杀订单表
- 秒杀订单明细表
- 秒杀活动表
- 用户表
- …
6、如何实现库存预热?
方案:秒杀活动开始前,使用定时任务等方式将商品的库存信息提前缓存到redis中,等到用户秒杀下单时,可以快速直接扣减redis中的商品库存,无需直接操作数据库
注意:为了保证查询库存和扣除库存这两个操作之间的原子性,可以借助redis的lua脚本
7、如何做好服务降级、熔断问题?
背景:为了避免某些服务/服务器发生宕机或服务不可用的情况,我们需要做一些服务熔断和降级措施,即进行快速失败
方案:使用一些中间件,如Alibaba Sentinel、Hystrix等
三、解决秒杀系统的常见问题
1、如何解决秒杀系统中超卖问题?
超卖指的是同一个时间有多个用户同时去抢单,即同时扣减多个库存,这时候就可能出现用户数大于redis缓存的库存数,出现超卖的问题
- 例子:比如商品只有10个,但是发现秒杀后卖了20个,那么有10个人是拿不到商品的
秒杀扣减库存分为两个步骤,分别为:
- 判断库存数是否充足
- 扣减库存
解决方案:使用redis的lua脚本,保证操作的原子性
2、如果redis减库存成功,但mq消费失败了怎么办?
可能出现问题:订单生成失败等,这就是秒杀系统的少卖问题
解决方案:
- mq添加重试机制:如果重试多次仍发送失败,则需要把该消息持久化(如存到MySQL中),并进行轮询重试处理,保证该消息能正常处理
- 持久化mq消息:增加一张消息发送表,在生产者发送mq之前,先把消息存到消息发送表中,并设置消息状态为待处理,然后再发送mq,消费者消费后,回调生产者的接口,修改消息的状态为已处理。这样如果在写表后发送失败,可以借助定时任务扫描待处理的消息,然后重新发送
- 监控告警:做好解决方案后,还是需要监控mq是否发送失败,以及重试后是否最终还是发送失败,并做对应的告警