开篇词 | 秒杀系统架构设计都有哪些关键点?
- 秒杀主要解决两个问题,一个是并发读,一个是并发写
- 秒杀的整体架构需要做到:稳、准、快。
01 | 设计秒杀系统时应该注意的5个架构原则
-
架构原则:“4 要 1 不要”
- 数据要尽量少
- 请求数要尽量少
- 路径要尽量短
- 依赖要尽量少
-
不要
有单点
架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。
02 | 如何才能做好动静分离?有哪些方案可选?
那到底什么才是动静分离呢?所谓“动静分离”,其实就是把用户请求的数据(如HTML页面)划分为“动态数据”和“静态数据”。
- 简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和URL、浏览者、时间、地域相关,以及是否含有Cookie等私密数据。
- 你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN上或者在服务端的Cache中。你应该根据情况,把它们尽量缓存到离用户最近的地方。
如何做动静分离的改造
-
我们如何把动态页面改造成适合缓存的静态页面呢?
-
URL唯一。商品详情系天然h就j以做到URL唯一化,比如每个商品都由
ID来标识,那么 http://item.xxx.com/item.htm?...
就可以作为唯一的URL标识。
- 分离浏览者相关的因素。浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。
- 分离时间因素。服务端输出的时间也通过动态请求获取。
- 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
- 去掉Cookie,服务端输出的页面包含的Cookie可以通过代码软件来删除,如Web服务器Varnish可以通过unset req.http.cookie命令去掉Cookie.
-
动态内容如何处理?
- ESI方案(或者SSI):即在Web代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面В已一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
- CSI方案。即单独发起一个异步JavaScript请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。
03 | 有针对性地处理好系统的热点数据
-
发现热点数据
- 通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。
- 通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。
-
怎么优化
- 优化热点数据最有效的办法就是缓存热点数据,如果热点数据做了动静分离,那么可以长期缓存静态数据。但是,缓存热点数据更多的是"临时”缓存,即不管是静态数据还是动态数据,都用一个队列短暂地缓存数秒钟,由于队列长度有限,可以采用LRU淘汰算法替换。
- 再来说说限制。限制更多的是一种保护机制,限制的办法也有很多,例如对被访问商品的ID做一致性Hash,然后根据Hash做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。
- 最后介绍一下隔离。秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让1%的请求影响另外的99%,隔离出也更方便对这1%的请求做针对性的优化。
04 | 流量削峰这事应该怎么做?
对秒杀这个场景来说,最终能够抢到商品的人数是固定的,就说100人和10000人发起请求的结果都一样,并发度越高,无效请求也越多。
但是从业务上来说,秒杀活动是希望更多的人来参与的,也就是开始之前希望有更多的人来刷页面,但真正开始下单,秒请求并不是越多越好。因此我们可以设计一些规则,让并发的请求更多地延缓,而且我们甚至可以过滤掉一些无效请求。
-
有损方案
-
无损方案
- 排队
- 消息队列
- 线程池加锁等待
- 把请求序列化到文件中,然后再顺序地读文件(例如基于MySQL binlog的同步机制)来恢复请求
秒杀系统中的常用削峰方法
-
答题
- 这个重要的功能就是把峰值的下单请求拉长,从以前的1s之内延长到2s-10s。还能防止机器抢单。
-
分层过滤
- 对请求进行分层过滤,从而过滤掉一些无效的请求。
- 浏览器层面:秒杀是否已经结束,答题是否正确
- 缓存:商品状态是否正常,用户是否具有秒杀资格,库存判断
- 数据:扣减库存
05 | 影响性能的因素有哪些?又该如何提高系统的性能?
-
减少编码
- 那么如何才能减少编码呢?例如,网页输出是可以直接进行流输出的,即用resp.getOutputstream() 函数写数据,把一些静态的数据提前转化成字节,等到真正往外写的时候再直接用OutputStream() 函数写,就可以减少静态数据的编码转换。
-
减少序列化
- 序列化大部分是在RPC中发生的,因此避免或者减少RPC就可以减少序列化,当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案,就是可以将多个关联性比较强的应用进行"合并部署",而减少不同应用之间的RPC也可以减少序列化的消耗。
- 所谓"合并部署",就是把两个原本在不同机器上的不同应用合并部署到一台机器上,当然不仅仅是部署在一台机器上,还要在同一个Tomcat容器中,且不能走本机的Socket,这样才能避免序列化的产生。
-
并发读优化
-
需要划分成动态数据和静态数据分别进行处理:
像商品中的“标题"和"描述"这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;
- 像库存这类动态数据,会采用"被动失效"的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据。
06 | 秒杀系统“减库存”设计的核心逻辑
- 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
- 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付т了款的情况,因为可能商品已经被其他人买走了。
- 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如10分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。
针对“库存超卖”这种情况,在10分钟时间内下单的数量仍然有可能超过库存数量,遇到这种情况我们只能区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。
实际使用方案
07 | 准备Plan B:如何设计兜底方案?
具体来说,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时。
- 架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转。
- 编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果。
- 测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。
- 发布阶段:发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急的回滚机制。
- 运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
- 故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及B下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。
针对秒杀系统,如何做到高可用?
-
降级
- 所谓"降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。
- 降级方案可以这样设计:当秒杀流量达到5w/s时,把成交记录的获取从展示20条降级到只展示5条。“从20改到5"这个操作由一个开关来实现,也就是设置一个能够从开关系统动态获取的系统参数。
-
限流
- 客户端限流,好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果的太大,则起т到限制的作用。
- 服务端限流,好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。
-
拒绝服务
- 在最前端的Nginx上设置过载保护,当机器负载达到某个值时直接拒绝HTTP请求并返回503错误码,在Java层同样也可以设计过载保护。
- 拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
原文
https://segmentfault.com/a/1190000020314341