Java微服务系统架构设计思路以及秒杀系统设计思路
前⾔:我们在做系统开发的时候,特别要清楚我们要做的是什么东西,我们想要的是什么,我们将来应该怎么做,这些都是我们开发的时候⾮常重要的事情,如果不能更好的了解这个,对于后期的开发会带来巨⼤的不确定性。当然这些很多属于业务和⽼板的想法决定。我们介绍来说的是软件开发中的问题。
⼀:设计思路
1:要有⾼内聚低耦合的设计思想。
在软件开发的过程中,我们遇到的⼤部分都是快速迭代模型的软件开发。因此我们本着⾼内聚,低耦合的设计原则。
2:单⼀责任原则(The single responsibility Principle)SRP
即是:修改⼀个类的原因应该只是⼀个。
换句话说就是让⼀个类只负责⼀件事,当这个类需要做过多事情的时候,就需要分解这个类。
如果⼀个类承担的职责过多,就等于把这些职责耦合在了⼀起,⼀个职责的变化可能会削弱这个类完成其他职责的能⼒。
3:开放封闭原则
即是:类应该对扩展开放,对修改关闭。
扩展就是添加新功能的意思,因此该原则需要在添加新功能时不需要修改代码
符合开闭原则最典型的设计模式是装饰者模式,它可以动态的将责任附加到对象上,⽽不⽤去修改类的代码
4:⾥⽒替换原则
即是:⼦类对象必须能够替换掉所有对象⽗类。
继承是⼀种IS-A关系。⼦类需要能够当成⽗类来使⽤。并且需要⽐⽗类更特殊。
如果不满⾜这个原则,那么各个⼦类的⾏为上就会有很⼤的差异,增加继承体系的复杂度。
5:接⼝分离原则。
即是:不应该强迫客户依赖它们不⽤的⽅法。
因此使⽤多个专门的接⼝⽐使⽤单⼀的总接⼝要好。
6:依赖倒置原则。
即是:⾼层模块不应该依赖于底层模块,⼆者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
⾼层模块包含⼀个应⽤程序中重要的策略选择和业务模块,如果⾼层模块依赖于底层模块,那么底层模块的改动就会直接影响到⾼层模块。从⽽迫使⾼层模块也需要改动。
依赖于抽象意味着:
任何变量都不应该持有⼀个指向具体类的指针或者引⽤。
任何类都不应该从具体类派⽣。
任何⽅法都不应该覆写它的任何基类中的已经实现的⽅法。
⼆:秒杀系统设计
1:场景
我们现场要卖100件单⼀商品,然后我们根据以往这样秒杀活动的数据经验来看,⽬测来抢这100件纸
尿裤的⼈⾜⾜有10万⼈。如果在同⼀时间内,⾼达10w的点击率,普通的服务完全受不了。
2:问题。
(1):⾼并发
秒杀特点就是时间短,瞬间⽤户量⼤,
⼤量的请求进来,我们需要考虑的点就很多了,缓存雪崩,缓存击穿,缓存穿透等等。
(2):超卖
如果没处理好库存问题,当商品只有100件,但是你却卖出去200件,这个问题怎么处理,发货还是不发货呢。
(3):恶意请求
我知道什么时候抢,我搞个⼏⼗台机器搞点脚本,我也模拟出来⼗⼏万个⼈左右的请求,那我是不是意味着我基本上有80%的成功率了。
要知道机器触发的速度完全超过⼈的速度。
(4):秒杀链接暴露
我们通常是点击按钮触发请求,这样懂⾏的⽤开发者模式就能看到请求,这样就可以模拟请求。
(5):数据库极限
每秒上万甚⾄⼗⼏万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,⽽且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的⼀起挂,⼩公司的话可能全站崩溃404。
3:解决问题。
(1):单⼀原则
我们在说设计思路的时候,有⼀个单⼀原则。这样可以解决很多问题。
⼤家都知道现在设计都是微服务的设计思想,然后再⽤分布式的部署⽅式
也就是我们下单是有个订单服务,⽤户登录管理等有个⽤户服务等等,那秒杀也开个服务,我们把秒杀的代码业务逻辑放⼀起。单独给他建⽴⼀个数据库,现在的互联⽹架构部署都是分库的,⼀样的就
是订单服务对应订单库,秒杀我们也给他建⽴⾃⼰的秒杀库。⾄于表就看⼤家怎么设计了,该设置索引的地⽅还是要设置索引的,建完后记得⽤explain看看SQL的执⾏计划。(不了解的⼩伙伴也没事,MySQL章节我会说的)单⼀职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(强⾏⾼可⽤)
(2):秒杀链接加盐
我们上⾯说了链接要是提前暴露出去可能有⼈直接访问url就提前秒杀了,那⼜有⼩伙伴要说了我做个时间的校验就好了呀,那我告诉你,知道链接的地址⽐起页⾯⼈⼯点击的还是有很⼤优势。我知道url了,那我通过程序不断获取最新的北京时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对⽐你⼈⼯点的成功率⼤太多了,⽽且我可以⼀毫秒发送N次请求,搞不好你卖100个产品我全拿了。
那这种情况怎么避免?
简单,把URL动态化,就连写代码的⼈都不知道,你就通过MD5之类的加密算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。
public static String getKuaishouSign(String url) throws NoSuchAlgorithmException {
MessageDigest Instance("MD5");
md5.Bytes());
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("@");
}
buf.HexString(i));
}
String();
return url;
}
(3):Redis集化。
单个Redis是有极限的通常能处理4-6w的数据,但是我们的数据量过⼤的时候,我们就⽆法处理了。这个时候需要对Redis进⾏集化处理。Redis集,主从同步、读写分离,我们还搞点哨兵,开启持久化等等!
(4):Nginx的负载均衡。
Nginx是⾼性能的web服务,并发也随便顶⼏万不是梦,但是我们的Tomcat只能顶⼏百的并发呀,那简单呀负载均衡嘛,⼀台服务⼏百,那就多搞点,在秒杀的时候多租点流量机。
恶意请求拦截也需要⽤到它,⼀般单个⽤户请求次数太夸张,不像⼈为的请求在⽹关那⼀层就得拦截掉了,不然请求多了他抢不抢得到是⼀回事,服务器压⼒上去了,可能占⽤⽹络带宽或者把服务器打崩、缓存击穿等等。
(5):资源静态化:
秒杀⼀般都是特定的商品还有页⾯模板,现在⼀般都是前后端分离的,所以页⾯⼀般都是不会经过后端的,但是前端也要⾃⼰的服务器啊,那就把能提前放⼊cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做⼀下,减少真正秒杀时候服务器的压⼒。
(6):按钮控制:
⼤家有没有发现没到秒杀前,⼀般按钮都是置灰的,只有时间到了,才能点击。
这是因为怕⼤家在时间快到的最后⼏秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。
这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可⽤状态。
按钮可以点击之后也得给他置灰⼏秒,不然他⼀样在开始之后⼀直点的。你敢说你们秒杀的时候不是这样的?
(7):限流:
限流这⾥我觉得应该分为前端限流和后端限流。
前端限流:这个很简单,⼀般秒杀不会让你⼀直点的,⼀般都是点击⼀下或者两下然后⼏秒之后才可以继续点击,这也是保护服务器的⼀种⼿段。
后端限流:秒杀的时候肯定是涉及到后续的订单⽣成和⽀付等操作,但是都只是成功的幸运⼉才会⾛到那⼀步,那⼀旦100个产品卖光了,return了⼀个false,前端直接秒杀结束,然后你后端也关闭后续⽆效请求的介⼊了。
(8):库存预热
秒杀的本质,就是对库存的抢夺,每个秒杀的⽤户来你都去数据库查询库存校验库存,然后扣减库存。
分布式和微服务的关系我们都知道数据库顶不住⼤量的请求,但是他的兄弟⾮关系型的数据库Redis能顶的住!
我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis⾥⾯去做,然后等秒杀介绍了,再异步的去修改库存就好了。
但是⽤了Redis就有⼀个问题了,我们上⾯说了我们采⽤主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是⾼并发的情况问题就很⼤了。
但是我们还有个问题,解决超卖的问题。
Lua脚本是类似Redis事务,有⼀定的原⼦性,不会被其他命令插队,可以完成⼀些Redis事务性的操作。这点是关键。
知道原理了,我们就写⼀个脚本把判断库存扣减库存的操作都写在⼀个脚本丢给Redis去做,那到0了后⾯的都Return False了是吧,⼀个失败了你修改⼀个开关,直接挡住所有的请求,然后再做后⾯的事情嘛。
(9):削峰填⾕
你可以把它放消息队列,然后⼀点点消费去改库存就好了嘛,不过单个商品其实⼀次修改就够了,我这⾥说的是某个点多个商品⼀起秒杀的场景,像极了双⼗⼀零点。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论