写在前面: 建站以来,除了上传一些之前的笔记,就没再写新内容了,有点惭愧,也有些偏离了最初的设想。前段时间忙于毕业设计和实习,技术学习一度中断(毕设偏理论,难以落地成工程项目;算法方向也不是特别感兴趣,所以相关内容更新可能会比较少)。 预计七月进入秋招节奏,五一之后开始重新拾起技术栈,补了一些 Java,同时也在陆续复习八股、学习新内容。最近花时间看了一下秒杀系统设计,这是面试中常见的场景问题,顺便做点简单的记录和分享。
秒杀系统主要是做三件事情:
承接高并发的流量,削峰还是限频?
削峰就是使用消息队列,这样可以扛下很高的qps,但是产品体验不好,抢了一会发现抢不到。
所以一般还是使用限频。
库存扣减
小流量来讲,直接使用Mysql就好,但是对高并发场景,Mysql扛不住,Redis不可靠。所以本次设计使用的方案是Redis预扣。
扣库存 Redis只做预扣使用,即限频作用,只有Redis预扣成功的流量才会到Mysql。而真正的库存放在Mysql中。
Mysql库存扣减成功后,才会创建订单,前端跳转支付页,支付成功后更新状态。放弃支付的,库存重新更新回去。
Redis实现预扣,Mysql是实际的库存。也就是说,Redis中拿到了“库存”,也并不代表实际抢购成功,只是拿到了去Mysql获取真实库存的机会(当然这个机会成功的概率是非常高的,一般情况也不会有问题)。库存最终还是受Mysql的保护,如果出现了主从切换或其他问题导致的异常,这时之前拿到名额的人也不一定能够抢购成功。
除了Mysql和Redis,还是用了消息队列来再次保证系统的高可用。虽然说通过Redis拿到名额的流量已经减少了很多,但是如果商品数量(指一个mysql物理分片存储当前时间参与秒杀的所有商品数)过万,那么打到Mysql的瞬时流量也会很高,足以将Mysql打崩。
Mysql存储内容
Redis 存储内容
获取基本的商品信息和秒杀信息
发起抢购
2.1 扣减Redis 名额并记录秒杀信息。要记录秒杀当前状态。
2.2 发送mq
消费mq信息
3.1 扣减Mysql库存 3.2 通过订单系统创建订单 3.3 将秒杀记录写入表中 3.4 更新预扣系统中Redis秒杀信息状态。
在此流程中,客户端会一直轮询,当发现秒杀状态变为待支付时,跳转支付页面。
另外,流程中对于Mysql的处理,可以通过事务解决原子性,而Redis必须通过lua脚本保证单次操作的原子性。
通过上面的设计,几乎可以避免了超卖现象了
名额加载回Redis
通过定时任务对比Mysql和Redis库存。需要判断Mysql库存还在,且Redis名额为0,此时可能少卖了。为了避免还有请求没有处理,可以等待一段时间,确定少卖了,则将Mysql库存加载回Redis。
超时库存退回Mysql
定时任务扫描近X分钟的数据(比如15分钟),如果订单超时没有支付,额度加载回Mysql,通过乐观锁+事务实现幂等,此时可以出发上面加载Reids的操作(因为那么久大概率已经没有了)
一般想法可能是定时任务,比如十点开卖,那么十点启动一个任务去给Redis增加库存。但是仔细思考发现是有问题的,因为这需要时间,给用户的感觉会是开始抢购的时间不准。因此只能通过给Redis预加载库存来实现。
提供一种思路,来源是一个朋友分享的企业内部解决方案。预先给所有秒杀商品库存全部加载进去,比如A商品10点、16点开卖两次,每次1000单。那么可以在0点就将库存加载到Redis预扣库存中,但是在判断是否还有库存不再和0比较,而是10点前和2000比较,10点到16点和1000比较,16点之后和0比较。当然后续名额退回等操作也需要实现类似逻辑。具体实现方法可以自己构思,无论是中间加一层保存各商品不同时间应剩余的库存,还是给每个商品提供一个计算公式,计算库存使用该公式计算,我认为都可以。
可以针对IP限流、加入验证码(一般不用)
系统设计没有银弹,要根据具体情况判断,不同量级不同要求下架构设计一定不同,既要保证系统的高可用、避免超卖、尽量少卖,也应该避免“大炮打蚊子”。对于一些企业来讲,这种系统一定不够,还需要考虑分片分表、Redis以及Mysql集群部署等问题,也需要补充一定的降级方案。总之,本文仅记录自己学习的一点内容,大多数思路来源网上,同时补充了一点自己的思考,如有问题欢迎大家指正。
本文作者:AstralDex
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!