秒杀你们都不陌生。自2011年首次出现以来,不管是双十一购物仍是 12306 抢票,秒杀场景已随处可见。简单来讲,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大流量的秒杀系统须要进行哪些关注,就是本文讨论的话题。前端
首先从高维度出发,总体思考问题。秒杀无外乎解决两个核心问题,一是并发读,一是并发写,对应到架构设计,就是高可用、一致性和高性能的要求。关于秒杀系统的设计思考,本文即基于此 3 层依次推动,简述以下——git
你们可能会注意到,秒杀过程当中你是不须要刷新整个页面的,只有时间在不停跳动。这是由于通常都会对大流量的秒杀系统作系统的静态化改造,即数据意义上的动静分离。动静分离三步走:一、数据拆分;二、静态缓存;三、数据整合。github
动静分离的首要目的是将动态页面改形成适合缓存的静态页面。所以第一步就是分离出动态数据,主要从如下 2 个方面进行:数据库
这里你能够打开电商平台的一个秒杀页面,看看这个页面里都有哪些动静数据。后端
分离出动静态数据以后,第二步就是将静态数据进行合理的缓存,由此衍生出两个问题:一、怎么缓存;二、哪里缓存浏览器
1.2.1 怎么缓存缓存
静态化改造的一个特色是直接缓存整个 HTTP 链接而不是仅仅缓存静态数据,如此一来,Web 代理服务器根据请求 URL,能够直接取出对应的响应体而后直接返回,响应过程无需重组 HTTP 协议,也无需解析 HTTP 请求头。而做为缓存键,URL惟一化是必不可少的,只是对于商品系统,URL 自然是能够基于商品 ID 来进行惟一标识的,好比淘宝的 https://item.taobao.com/item....。安全
1.2.2 哪里缓存性能优化
静态数据缓存到哪里呢?能够有三种方式:一、浏览器;二、CDN ;三、服务端。服务器
浏览器固然是第一选择,但用户的浏览器是不可控的,主要体如今若是用户不主动刷新,系统很难主动地把消息推送给用户(注意,当讨论静态数据时,潜台词是 “相对不变”,言外之意是 “可能会变”),如此可能会致使用户端在很长一段时间内看到的信息都是错误的。对于秒杀系统,保证缓存能够在秒级时间内失效是不可或缺的。
服务端主要进行动态逻辑计算及加载,自己并不擅长处理大量链接,每一个链接消耗内存较多,同时 Servlet 容器解析 HTTP 较慢,容易侵占逻辑计算资源;另外,静态数据下沉至此也会拉长请求路径。
所以一般将静态数据缓存在 CDN,其自己更擅长处理大并发的静态文件请求,既能够作到主动失效,又离用户尽量近,同时规避 Java 语言层面的弱点。须要注意的是,上 CDN 有如下几个问题须要解决:
所以,将数据放到全国全部的 CDN 节点是不太现实的,失效问题、命中率问题都会面临比较大的挑战。更为可行的作法是选择若干 CDN 节点进行静态化改造,节点的选取一般须要知足如下几个条件:
基于以上因素,选择 CDN 的二级缓存比较合适,由于二级缓存数量偏少,容量也更大,访问量相对集中,这样就能够较好解决缓存的失效问题以及命中率问题,是当前比较理想的一种 CDN 化方案。部署方式以下图所示:
分离出动静态数据以后,前端如何组织数据页就是一个新的问题,主要在于动态数据的加载处理,一般有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
动静分离对于性能的提高,抽象起来只有两点,一是数据要尽可能少,以便减小不必的请求,二是路径要尽可能短,以便提升单次请求的效率。具体方法其实就是基于这个大方向进行的。
热点分为热点操做和热点数据,如下分开进行讨论。
零点刷新、零点下单、零点添加购物车等都属于热点操做。热点操做是用户的行为,很差改变,但能够作一些限制保护,好比用户频繁刷新页面时进行提示阻断。
热点数据的处理三步走,一是热点识别,二是热点隔离,三是热点优化。
热点数据分为静态热点和动态热点,具体以下:
所以秒杀系统须要实现热点数据的动态发现能力,一个常见的实现思路是:
须要注意的是:
热点数据识别出来以后,第一原则就是将热点数据隔离出来,不要让 1% 影响到另外的 99%,能够基于如下几个层次实现热点隔离:
固然,实现隔离还有不少种办法。好比,能够按照用户来区分,为不一样的用户分配不一样的 Cookie,入口层路由到不一样的服务接口中;再好比,域名保持一致,但后端调用不一样的服务接口;又或者在数据层给数据打标进行区分等等,这些措施的目的都是把已经识别的热点请求和普通请求区分开来。
热点数据隔离以后,也就方便对这 1% 的请求作针对性的优化,方式无外乎两种:
数据的热点优化与动静分离是不同的,热点优化是基于二八原则对数据进行了纵向拆分,以便进行针对性地处理。热点识别和隔离不只对“秒杀”这个场景有意义,对其余的高性能分布式系统也很是有参考价值。
对于一个软件系统,提升性能能够有不少种手段,如提高硬件水平、调优JVM 性能,这里主要关注代码层面的性能优化——
性能优化须要一个基准值,因此系统还须要作好应用基线,好比性能基线(什么时候性能忽然降低)、成本基线(去年大促用了多少机器)、链路基线(核心流程发生了哪些变化),经过基线持续关注系统性能,促使系统在代码层面持续提高编码质量、业务层面及时下掉不合理调用、架构层面不断优化改进。
秒杀系统中,库存是个关键数据,卖不出去是个问题,超卖更是个问题。秒杀场景下的一致性问题,主要就是库存扣减的准确性问题。
电商场景下的购买过程通常分为两步:下单和付款。“提交订单”即为下单,“支付订单”即为付款。基于此设定,减库存通常有如下几个方式:
可以看到,减库存方式是基于购物过程的多阶段进行划分的,但不管是在下单阶段仍是付款阶段,都会存在一些问题,下面进行具体分析。
优点:用户体验最好。下单减库存是最简单的减库存方式,也是控制最精确的一种。下单时能够直接经过数据库事务机制控制商品库存,因此必定不会出现已下单却付不了款的状况。
劣势:可能卖不出去。正常状况下,买家下单后付款几率很高,因此不会有太大问题。但有一种场景例外,就是当卖家参加某个促销活动时,竞争对手经过恶意下单的方式将该商品所有下单,致使库存清零,那么这就不能正常售卖了——要知道,恶意下单的人是不会真正付款的,这正是 “下单减库存” 的不足之处。
优点:必定实际售卖。“下单减库存” 可能致使恶意下单,从而影响卖家的商品销售, “付款减库存” 因为须要付出真金白银,能够有效避免。
劣势:用户体验较差。用户下单后,不必定会实际付款,假设有 100 件商品,就可能出现 200 人下单成功的状况,由于下单时不会减库存,因此也就可能出现下单成功数远远超过真正库存数的状况,这尤为会发生在大促的热门商品上。如此一来就会致使不少买家下单成功后却付不了款,购物体验天然是比较差的。
优点:缓解了以上两种方式的问题。预扣库存实际就是“下单减库存”和 “付款减库存”两种方式的结合,将两次操做进行了先后关联,下单时预扣库存,付款时释放库存。
劣势:并无完全解决以上问题。好比针对恶意下单的场景,虽然能够把有效付款时间设置为 10 分钟,但恶意买家彻底能够在 10 分钟以后再次下单。
减库存的问题主要体如今用户体验和商业诉求两方面,其本质缘由在于购物过程存在两步甚至多步操做,在不一样阶段减库存,容易存在被恶意利用的漏洞。
业界最为常见的是预扣库存。不管是外卖点餐仍是电商购物,下单后通常都有个 “有效付款时间”,超过该时间订单自动释放,这就是典型的预扣库存方案。但如上所述,预扣库存还须要解决恶意下单的问题,保证商品卖的出去;另外一方面,如何避免超卖,也是一个痛点。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
业务手段保证商品卖的出去,技术手段保证商品不会超卖,库存问题历来就不是简单的技术难题,解决问题的视角是多种多样的。
库存是个关键数据,更是个热点数据。对系统来讲,热点的实际影响就是 “高读” 和 “高写”,也是秒杀场景下最为核心的一个技术难题。
秒杀场景解决高并发读问题,关键词是“分层校验”。即在读链路时,只进行不影响性能的检查操做,如用户是否具备秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求等,而不作一致性校验等容易引起瓶颈的检查操做;直到写链路时,才对库存作一致性检查,在数据层保证最终准确性。
所以,在分层校验设定下,系统能够采用分布式缓存甚至LocalCache来抵抗高并发读。即容许读场景下必定的脏数据,这样只会致使少许本来无库存的下单请求被误认为是有库存的,等到真正写数据时再保证最终一致性,由此作到高可用和一致性之间的平衡。
实际上,分层校验的核心思想是:不一样层次尽量过滤掉无效请求,只在“漏斗” 最末端进行有效处理,从而缩短系统瓶颈的影响路径。
高并发写的优化方式,一种是更换DB选型,一种是优化DB性能,如下分别进行讨论。
4.2.1 更换DB选型
秒杀商品和普通商品的减库存是有差别的,核心区别在数据量级小、交易时间短,所以可否把秒杀减库存直接放到缓存系统中实现呢,也就是直接在一个带有持久化功能的缓存中进行减库存操做,好比 Redis?
若是减库存逻辑很是单一的话,好比没有复杂的 SKU 库存和总库存这种联动关系的话,我的认为是彻底能够的。但若是有比较复杂的减库存逻辑,或者须要使用到事务,那就必须在数据库中完成减库存操做。
4.2.2 优化DB性能
库存数据落地到数据库实现实际上是一行存储(MySQL),所以会有大量线程来竞争 InnoDB 行锁。但并发越高,等待线程就会越多,TPS 降低,RT 上升,吞吐量会受到严重影响——注意,这里假设数据库已基于上文【性能优化】完成数据隔离,以便于讨论聚焦 。
解决并发锁的问题,有两种办法:
高读和高写的两种处理方式截然不同。读请求的优化空间要大一些,而写请求的瓶颈通常都在存储层,优化思路的本质仍是基于 CAP 理论作平衡。
固然,减库存还有不少细节问题,例如预扣的库存超时后如何进行回补,再好比第三方支付如何保证减库存和付款时的状态一致性,这些也是很大的挑战。
盯过秒杀流量监控的话,会发现它不是一条蜿蜒而起的曲线,而是一条挺拔的直线,这是由于秒杀请求高度集中于某一特定的时间点。这样一来就会形成一个特别高的零点峰值,而对资源的消耗也几乎是瞬时的。因此秒杀系统的可用性保护是不可或缺的。
对于秒杀的目标场景,最终可以抢到商品的人数是固定的,不管 100 人和 10000 人参加结果都是同样的,即有效请求额度是有限的。并发度越高,无效请求也就越多。但秒杀做为一种商业营销手段,活动开始以前是但愿有更多的人来刷页面,只是真正开始后,秒杀请求不是越多越好。所以系统能够设计一些规则,人为的延缓秒杀请求,甚至能够过滤掉一些无效请求。
早期秒杀只是简单的点击秒杀按钮,后来才增长了答题。为何要增长答题呢?主要是经过提高购买的复杂度,达到两个目的:
须要注意的是,答题除了作正确性验证,还须要对提交时间作验证,好比<1s 人为操做的可能性就很小,能够进一步防止机器答题的状况。
答题目前已经使用的很是广泛了,本质是经过在入口层削减流量,从而让系统更好地支撑瞬时峰值。
最为常见的削峰方案是使用消息队列,经过把同步的直接调用转换成异步的间接推送缓冲瞬时流量。除了消息队列,相似的排队方案还有不少,例如:
排队方式的弊端也是显而易见的,主要有两点:
排队本质是在业务层将一步操做转变成两步操做,从而起到缓冲的做用,但鉴于此种方式的弊端,最终仍是要基于业务量级和秒杀场景作出妥协和平衡。
过滤的核心结构在于分层,经过在不一样层次过滤掉无效请求,达到数据读写的精准触发。常见的过滤主要有如下几层:
一、读限流:对读请求作限流保护,将超出系统承载能力的请求过滤掉
二、读缓存:对读请求作数据缓存,将重复的请求过滤掉
三、写限流:对写请求作限流保护,将超出系统承载能力的请求过滤掉
四、写校验:对写请求作一致性校验,只保留最终的有效数据
过滤的核心目的是经过减小无效请求的数据IO保障有效请求的IO性能。
系统能够经过入口层的答题、业务层的排队、数据层的过滤达到流量削峰的目的,本质是在寻求商业诉求与架构性能之间的平衡。另外,新的削峰手段也层出不穷,以业务切入居多,好比零点大促时同步发放优惠券或发起抽奖活动,将一部分流量分散到其余系统,这样也能起到削峰的做用。
当一个系统面临持续的高峰流量时,实际上是很难单靠自身调整来恢复状态的,平常运维没有人可以预估全部状况,意外老是没法避免。尤为在秒杀这一场景下,为了保证系统的高可用,必须设计一个 Plan B 方案来进行兜底。
高可用建设,实际上是一个系统工程,贯穿在系统建设的整个生命周期。
具体来讲,系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,以及故障发生时,逐一进行分析:
对于平常运维而言,高可用更可能是针对运行阶段而言的,此阶段须要额外进行增强建设,主要有如下几种手段:
在系统建设的整个生命周期中,每一个环节中均可能犯错,甚至有些环节犯的错,后面是没法弥补的或者成本极高的。因此高可用是一个系统工程,必须放到整个生命周期中进行全面考虑。同时,考虑到服务的增加性,高可用更须要长期规划并进行体系化建设。
高可用实际上是在说 “稳定性”,稳定性是一个平时不重要,但出了问题就要命的事情,然而它的落地又是一个问题——平时业务发展良好,稳定性建设就会降级给业务让路。解决这个问题必须在组织上有所保障,好比让业务负责人背上稳定性绩效指标,同时在部门中创建稳定性建设小组,小组成员由每条线的核心力量兼任,绩效由稳定性负责人来打分,这样就能够把体系化的建设任务落实到具体的业务系统中了。
一个秒杀系统的设计,能够根据不一样级别的流量,由简单到复杂打造出不一样的架构,本质是各方面的取舍和权衡。固然,你可能注意到,本文并无涉及具体的选型方案,由于这些对于架构来讲并不重要,做为架构师,应该时刻提醒本身主线是什么。
同时也在这里抽象、提炼一下,主要是我的对于秒杀设计的提纲式整理,方便各位同窗进行参考——**