一. 秒杀背景分析
分析:商场要拿出来100个iphone11 pro来搞低价秒杀活动,预计0点会有10万个人来抢,会出现以下的问题:
1. 高并发
是的高并发这个是我们想都不用想的一个点,一瞬间这么多人进来这不是高并发什么时候是呢?
是吧,秒杀的特点就是这样时间极短、 瞬间用户量大。正常的店铺营销都是用极低的价格配合上短信、APP的精准推送,吸引特别多的用户来参与这场秒杀,爽了商家苦了开发呀。秒杀大家都知道如果真的营销到位,价格诱人,几十万的流量我觉得完全不是问题,那单机的Redis我感觉3-4W的QPS还是能顶得住的,但是再高了就没办法了,那这个数据随便搞个热销商品的秒杀可能都不止了。
大量的请求进来,我们需要考虑的点就很多了,缓存雪崩,缓存击穿,缓存穿透这些我之前提到的点都是有可能发生的,出现问题打挂DB那就很难受了,活动失败用户体验差,活动人气没了,最后背锅的还是开发。
2. 超卖
由于程序没有好库存问题,很容易出现超卖问题,商家的预算经费卖100个可以赚点还可以造势,结果你写错程序多卖出去200个,你不发货用户投诉你,平台封你店,你发货就血亏,你怎么办?
3. 恶意请求
你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛…)肯定也知道的。那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。
Tip:科普下,小道消息了解到的,黄牛的抢票系统,比国内很多小公司的系统还吊很多,架构设计都是顶级的,我用顶配的服务加上顶配的架构设计,你还想看演唱会?还想回家?
4. 链接暴露
懂点行的仔都可以打开谷歌的开发者模式,然后看看你的网页代码,有的就有URL,但是我写VUE的时候是事件触发然后去调用文件里面的接口看源码看不到,但是我可以点击一下查看你的请求地址啊,不过你好像可以对按钮在秒杀前置灰。
不管怎么样子都有危险,撇开外面的所有的东西你都挡住了,你卖这个东西实在便宜得过分,有诱惑力,你能保证开发不动心?开发知道地址,在秒杀的时候自己提前请求。
5. 数据库宕机
每秒上万甚至十几万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,而且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的一起挂,小公司的话可能全站崩溃404。反正不管你秒杀怎么挂,你别把别的搞挂了对吧,搞挂了就不是杀一个程序员能搞定的。
二. 问题逐个击破
1. 服务单一职责原则
设计个能抗住高并发的系统,我觉得还是得单一职责。什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式。
也就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。单独给他建立一个数据库,现在的互联网架构部署都是分库的,一样的就是订单服务对应订单库,秒杀我们也给他建立自己的秒杀库。
至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。
单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(强行高可用)
2. 秒杀链接加盐
我们上面说了链接要是提前暴露出去可能有人直接访问url就提前秒杀了,那又有小伙伴要说了我做个时间的校验就好了呀,那我告诉你,知道链接的地址比起页面人工点击的还是有很大优势。我知道url了,那我通过程序不断获取最新的北京时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对比你人工点的成功率大太多了,而且我可以一毫秒发送N次请求,搞不好你卖100个产品我全拿了。
3. Redis集群/库存预热
秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因数,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。那怎么办?
我们都知道数据库顶不住但是他的兄弟非关系型的数据库Redis能顶啊!
那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。(此种方案详见下面的分析)
单体Redis不够的话可以搞Redis集群,详见集群章节。
4. Nginx
Nginx大家想必都不陌生了吧,这玩意是高性能的web服务器,并发也随便顶几万不是梦,但是我们的IIS只能顶几千的并发呀,那简单呀负载均衡嘛,一台服务几百,那就多搞点,在秒杀的时候多租点流量机。
5. 资源静态化
秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所以页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。
6. 按钮控制
大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。这是因为怕大家在时间快到的最后几秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可用状态。
按钮可以点击之后也得给他置灰几秒,不然他一样在开始之后一直点的。你敢说你们秒杀的时候不是这样的?
7. 限流&降级&熔断&隔离:
前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。
后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。
Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。
这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了,限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。
8. 削峰填谷
一说到这个名词,很多小伙伴就知道了,对的MQ(消息队列),你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢?服务器挂了,程序员又要背锅的。
Tip:可能小伙伴说我们业务达不到这个量级,没必要。但是我想说我们写代码,就不应该写出有逻辑漏洞的代码。
你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。
三. 针对超卖问题的几种解决思路
1. 加锁
(1). 单个IIS(即单进程),通过在代码里lock/monitor线程锁即可。
(2). 如果是IIS集群,多个秒杀集群同时请求DB,那么lock线程锁就锁不住了,这里就需要引入Redis分布式锁,所有请求先去Redis加锁→秒杀→解锁,实际上多个IIS请求最终到数据库 依次先后进行的。
PS:这个时候,如果说Redis扛不住这个并发,搞Redis集群,Redis集群中的锁也是依次进行的,集群中必须上一个redis解锁,下一个才能加锁,所以这里引入Redis集群,是为了应对高并发问题,是为了防止redis宕机,并不能加快最终秒杀的速度,实际上和单体Redis速度是一样的。
(redis使用的异步的方式,虽然是单线程,但是一直在运行,这恰巧解释了redis是依次串行执行,但还会有高并发问题)
2. 库存放redis里
思路:我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。
分析:
(1). 单体Redis没问题,提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做(利用Redis单线程院子),然后等秒杀介绍了,再异步的去修改库存就好了。
(2). 集群Redis乍一看有问题(比如1主多从)。
比如现在库存只剩下1个了,我们高并发嘛,4个redis服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3。
我们可以这样解决(比较高级):
Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。
Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
知道原理了,我们就写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求,然后再做后面的事情嘛。
3. 请求消息队列里
(1).单体或集群Redis
把所有的请求放到Redis队列中,然后开启一个线程去出队执行秒杀业务(秒杀业务执行完,下一个才出队),实际上到数据库还是 一个接着一个进行。(仅这一种场景可以)
注意:
A. 如果是开多个线程出队执行秒杀业务, 或者 即使单线程出队,然后秒杀 ,出队和秒杀是分开执行, 同样会存在上一个秒杀业务没有执行完,下一个又进来了。
B. 如果秒杀业务是集群,即使是依次出队,打到DB上还是会存在上述问题,且没法保证原先队列中的先后顺序了。
(2). 据说小米的解决方案:(不立即操作)
大家先发起抢的请求,服务器端把请求按先到先存规则放进消息队列,当消息队列到了最大值就说明是理论上的抢完了, 这时再处理消息队列生成订单, 大家过几分钟就能发现有没有抢得到, 抢到的让他们继续付款。
四. 架构图
下图中的Redis用的是简单的读写分离,即第一代架构模型,实际项目中建议改为第三代架构模型,Cluster模式。
第三代Redis架构模型:
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。