前言
设计一个系统之前,我们肯定要先确认系统业务场景是怎样的,下面就以某电商平台上的秒杀活动为场景,一起来探讨一个秒杀系统改如何去设计。
场景
我们现在要卖100件纸尿布,按照系统的用户量及以往经验来看,目测这样的秒杀活动会有10万人参加来抢这100件纸尿布。
10万人,这服务器哪里顶得住啊!如果打在DB上那肯定得挂啊。但是我们做点什么嘛。下面好好分析一下,秒杀系统可能会出现什么问题。
常见问题
高并发:
高并发这一点是毋庸置疑的,秒杀开始的一瞬间这么多用户访问系统,肯定是会出现高并发问题的。
是的,秒杀的特点就是这样时间极短、瞬间用户量大。
正常的店铺营销都是用极低的价格,再配合短信、APP等平台的精准推送,吸引了大量的用户来参加这场秒杀活动,这么多的用户量,商家肯定爽上天了,只是我们就苦逼了。。。
我们都知道,秒杀活动宣传到位、价格诱人的话,几十万的流量完全不是问题,而单机的Redis 一般来说3-4万的QPS(每秒请求数)还是能顶得住的,但是再高了就没办法了,那这个3-4万的数据随便搞个热销商品的秒杀都不止这个数了。
大量的请求涌入,我们需要需要考虑的问题就比较多了,缓存雪崩、缓存击穿、缓存穿透这些都有可能发生,出现了这些问题直接打在DB上,DB肯定扛不住这么大的流量,那这个秒杀活动也算是凉了。。。
库存超卖:
但凡是秒杀,都怕库存超卖,我们这里只是卖尿不湿,顶多也就不赚钱而已,要是换成100台华为Mate30Pro,商家预算卖100台可以赚点零花钱,还能造势,结果写错程序多卖出去了200台,不发货用户会投诉你、平台也会关你店,你发货就裤衩都赔进去了,这可不是杀两个程序猿祭天就能解决的事。
秒杀本来就是以低价吸引顾客来参与的,基本上都是不赚钱的,超卖就很恐怖了,所以库存超卖也是很关键的一个点。
恶意请求:
秒杀这么低的价格,如果我抢到了,我转手一卖大保健的钱又有了。。。就算我不卖那我也不亏,用户知道,我们能知道,某些有心人(黑客、黄牛。。)肯定也知道的。
这就简单了,我知道你什么时候活动开始(公开的),我搞几台服务器,再搞几个脚本,我也模拟出来十几万个人左右的请求,这是不是意味着80%的单子都来我这里了?
真是情况可能远远不止,因为机器请求的速度比人的手速往往快很多很多,每年过年抢票我们我们抢不过黄牛也是有一定道理的(好想宰一头黄牛祭天!!!)。。。
链接暴露:
前面几个问题我们都比较好理解,但是这个链接暴露估计就有不少的兄die会比较迷惑了。
相信我们对这个画面并不陌生,稍微懂点的都知道打开开发者模式,然后看看网页的代码,有的网页就写有url。对于一些查看不到源码的,我们可以点击一下查看对应的请求地址啊,不过好像这个可以对按钮在秒杀前置灰。
不管怎么样子都是有危险的,姑且算你外面的所有东西你都能挡住了,如果你卖的这个东西实在是便宜,极具诱惑力,你能保证开发不动心?开发知道地址,在秒杀的时候自己提前请求。。。(开发:为什么TM又是我???)
数据库:
每秒上万甚至十几万的QPS(每秒请求数)直接打在数据库上,基本上都能把数据库打挂,而且你服务器不单单是做秒杀的,还涉及到其他的业务,你没有做降级、限流、熔断等处理,直接挂一片,小公司的话可能是全站崩溃404。
反正不管你秒杀怎么挂,别把我其他的业务整挂就行了。要真挂一片了,这就不是随便杀一两个程序猿就能糊弄过去的事了。
程序猿:我。。。我TM挖谁祖坟了吗???
问题是讲出来了,但你别把问题丢给大佬呀,大佬要的是设计,你得给大佬讲清楚怎么设计,怎么解决这些问题呀。
设计一个能扛住高并发的系统,我觉得还是得单一职责。
什么意思呢?我们都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式。
也就是我们下单是有个订单服务,用户登录管理有用户服务等。既然这样,我们为何不能给秒杀也开一个服务呢?
我们把秒杀的代码业务逻辑放一起。单独给他建立一个数据库,现在的互联网架构部署都是分库的,一样的就是订单服务对应订单库,秒杀我们也给他建立自己的秒杀库。
至于表怎么设计,就看大家的设计习惯了,该设置索引的地方还是要设置索引的,建完后记得用 explain 看看 SQL 的执行计划。
单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务器挂了,也不会影响到其他的服务。(勉强算是高可用吧)
秒杀链接加盐:
我们上面提到了链接要是提前暴露出去就有可能被人直接访问 url 就提前秒杀了。这位兄die就有话说了:我做个时间的校验就好了。那我告诉你,知道链接地址比起页面人工点击的还是有很大优势的。
我知道 url 了,那我可以通过程序不断地获取北京的最新时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对比你人工点的成功率高很多,而且我可以一毫秒发送N次请求,一个不小心100个产品都是我的了。
这种情况要怎么避免呢?
这就比较简单了,把 url 动态化,就连写代码的人都不知道,你就通过MD5之类的加密算法,随机加密的字符串去做 url,然后通过前端代码获取 url 后台校验才能通过。
下面放一个简单的 url 加密给各位参考一下。
/**
* url md5加密
* @param url
* @return url
*/
public static String getURLsign(String url) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update((url).getBytes("UTF-8"));
byte[] b = md5.digest(); int i ;
StringBuffer buf = new StringBuffer();
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0) {
i += 256;
}
if (i < 16) {
buf.append("0");
}
buf.append(Integer.toHexString(i));
}
url = buf.toString();
System.out.println("result = " + url); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
logger.err("error:" ,e);
}
return url;
}
Redis集群:
之前不是说了单机的Redis顶不住嘛,那简单啊,一个不行就找两个,两个不行就整三个嘛,多整几个兄弟一起扛(春哥:肆胸弟就来砍我。。。)。
秒杀本来就是读多写少,这就需要用到我们提到过的 Redis集群、主从同步、读写分离了,我们还搞点哨兵,开启持久化直接无敌高可用了!
Nginx:
Nginx 相比大家都不陌生了,这东西是高性能的web服务器,并发也随便顶个几万不是梦,但是我们 Tomcat 只能订几百的并发呀。
那简单呀,负载均衡嘛,一台服务几百,那就多搞几台就好了嘛(这是几台能解决的事?),在秒杀的时候多租点流量机。
Tip:据网传,我国内某大厂就是在去年春节活动期间,租光了亚洲所有的服务器,小公司也很喜欢在双十一期间买流量来顶住压力。
这样一对比,是不是觉得你的集群能顶半边天了。
恶意请求拦截也需要用到它,一般单个用户请求次数太夸张,不像人为的请求在网关那一层就得拦截掉,不然请求多了他抢不抢得到是一回事,服务器压力上去了,可能占用网络宽带或者把服务器打崩、缓存击穿等。
资源静态化:
秒杀一般都是特定的商品和特定的页面模板,现在一般都是前后端分离的,所以页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入 cdn服务器 的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。
按钮控制:
大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。
这是因为怕用户在时间快到的最后几秒疯狂点击、请求服务器,然后一个不小心,秒杀还没开始基本上服务器就挂了。
这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新北京时间,到时间点再给按钮置成可用状态。
按钮可以点击之后也得给他置灰几秒,不然用户一样在开始之后一直点。
限流:
限流这点,可以分为前端限流和后端限流。
前端限流:
这个比较简单,一版秒杀不会让你一直点的,一般都是点击一下或者两下,然后几秒之后才可以继续点击,这也是保护服务器的一种手段。
后端限流:
秒杀的时候肯定涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖完了,return 了一个 false,前端直接秒杀结束,然后你后端也关闭后续无效请求的接入了。
Tip:真正的限流还会有限流组件的加入,例如:阿里的Sentinel、Hystrix等。这就就不展开,就说一下物理的限流。
库存预热:
秒杀的本质,就是对库存的抢夺,每个秒杀的用户来,你都去查一下数据库校验库存,然后扣减库存,撇开性能因素,你不觉得这样很繁琐,对业务开发人员都不友好,而且数据库顶不住啊。
那怎么办呢?
我们都知道数据库顶不住,但是他的兄弟(非关系型数据库Redis)能顶啊!
这就好说了,我们在开始秒杀前,通过定时任务或者运维同学提前把商品的库存加载都Redis中去,让整个流程都在Redis里面去做,然后等秒杀结束了,再异步的去修改库存就好了。
但是用了Redis就有一个问题了,我们上面说了要采用主从,就是我们会去读取库存,然后再判断,之后有库存再去减库存,正常情况下是很OK的,但是高并发的情况下问题就很大了。
现在库存只剩下1个了,高并发嘛,假设4个服务器一起查询了,发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,结果就变成 -3。是的,有一个是真的抢到了,但别的都超卖了。咋整?赔钱咯。。。
Lua:
不要慌,赔几个钱而已,问题不大。。。下面说个神器,连几个钱也不用赔。
Lua 脚本功能是 Redis 在2.6版本的最大亮点,通过内嵌对 Lua 环境的支持,Reids 解决了长久以来不能高效地处理 CAS(check-and-set)命令的缺点,并且可以通过组合使用多个命令,轻松实现以前很难实现或者不能高效实现的模式。
Lua 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 Redis 事务性的操作,这点是关键。
知道了原理,我们就写一个脚本把判断库存和扣减库存的操作都写在一个脚本,丢给 Redis 去做,到0后面的都 return false 了,一个失败了你修改一个开关,直接挡住了所有的请求,然后再做后面的事情嘛。
限流&降级&熔断&隔离:
这个为啥要做呢?不怕一万就怕万一,万一真顶不住了,限流,顶不住就挡一部分出去,但是不能说不行;降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就是独立的,但是你会调用其他系统嘛,你快不行了你也不能拖累其他兄弟呀。
削峰填谷:
一说到这个词,很多兄die就知道了,对的,MQ,你买东西少了,你直接100个请求改库我觉得也没啥问题,但是万一秒杀一万个呢?服务器挂了,又要杀程序猿祭天了。。。
Tip:可能有的兄die说,我们业务量达不到这个量级,没必要。但是我想说我们写代码,就不应该写出有逻辑漏洞的代码,至少以后公司体量上去了,代码居然不用改。小伙子这手代码写得真漂亮!
总结:
到这里,我觉得基本上把该考虑的店,还有对应的解决方案都说了一下,也许还有没有考虑到的,欢迎补充哈。
最后放个完整的流程图出来吧。