如何设计一个秒杀系统----学习总结

第一章学习总结——概览https://time.geekbang.org/column/article/40153
1.秒杀主要解决问题——并发读和并发写。并发读的核心优化理念是尽量减少用户到服务端来读取数据,或者让他们读更少的数据。并发写的处理原则是在数据库层面独立出一个库,做特殊的处理。另外针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。

2.从一个架构师的角度来看,要想打造并维护一个超大流量并发读写、高性能、高可用的系统,在整个用户请求路径上从浏览器到服务端我们要遵循几个原则,就是要保证用户请求的数据尽量少、请求数尽量少、路径尽量短、依赖尽量少,并且不要有单点。

3.秒杀的整体架构可以概括为“稳、准、快”(高可用、一致性、高性能)几个关键字。
所谓“稳”,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。

“准”,就是秒杀 10 台 iPhone,那就只能成交 10 台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。

“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。

所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求。

高性能:
秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。主要包含数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这 4 个方面。

一致性:
秒杀中商品减库存的实现方式同样关键。有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性。

高可用:
现实中总难免出现一些考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。
——————————————————————————————————————————————————————————————————————————————————
第二章学习总结——设计秒杀系统应该注意的五个(4要1不要)架构原则。https://time.geekbang.org/column/article/40726
1.数据量尽量少:请求数据和返回数据都要尽量少,以减少CPU使用。

2.请求数要尽量少:减少额外请求,如合并js、css等,首屏HTML内联所需的CSS、JS。

3.路径要尽量短:减少节点数,相互强依赖的应用合并部署。节点越少出错概率就越小,可用性就越高。所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。

4.强依赖尽量少:给数据重要性分等级,尽量减少所要加载的数据。

5.不要有单点,要有备份,如设计分布式系统,关键点是把服务无状态话,避免将服务的状态和机器绑定,使服务可以在机器中随意移动。

架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。所以设计架构时上面几点只是方向,应该根据实际情况适当取舍。

补充:
答疑:
1 .本地cache用什么实现好呢?
本地cache一般就是用内存实现,如java用集合类型就行
2. 通过什么方式往本地cache 写数据呢?
用订阅的方式,在初始化时加载到内存
3. 秒杀系统的及时性非常高,把库存写进cache ,怎么及时更新呢?
有两种方法,一是定时更新取3秒,二是,主动更新,数据库字段更新后发消息更新缓存,这个需要用到一个组件阿里叫metaq就是就是数据库字段更新会产生一条消息。另外cache里库存不需要100%和数据库一致,最终强一致性即可
4.各QPS级别架构可能瓶颈点?
不同QPS量级下瓶颈也会不一样,10w级别可能瓶颈就在数据读取上,通过增加缓存一般就能解决,如果要到100w那么,可能服务端的网络都是瓶颈,所以要把大部分的静态数据放到cdn上甚至缓存在浏览器里。
5.单点是什么?
单点就是一个状态值存储在一台机器上,这台机器挂了,这个状态就丢了,导致整个服务不可用。最常用的就是本机存储数据,而这个数据没有备份的情况

架构示例:
1-10万QPS级别架构设计示例图:

100万级别QPS架构示意图:

————————————————————————————————————————————————————————————————————————————————————
第三章:如何才能做好动静分离?有哪些方案可选?https://time.geekbang.org/column/article/40727
1.what?

2.how?

3.动态内容的处理方式:

4.静态数据的处理方式:

5.动静分离的几种架构:
实体机单机部署;统一Cache层;上CDN。

6.问题一:
存储在浏览器或 CDN 上,有多大区别?
在 CDN 上,我们可以做主动失效,而在用户的浏览器里就更不可控,如果用户不主动刷新的话,你很难主动地把消息推送给用户的浏览器。

7.问题二:
在什么地方把静态数据和动态数据合并并渲染出一个完整的页面也很关键,假如在用户的浏览器里合并,那么服务端可以减少渲染整个页面的 CPU 消耗。如果在服务端合并的话,就要考虑缓存的数据是否进行 Gzip 压缩了:如果缓存 Gzip 压缩后的静态数据可以减少缓存的数据量,但是进行页面合并渲染时就要先解压,然后再压缩完整的页面数据输出给用户;如果缓存未压缩的静态数据,这样不用解压静态数据,但是会增加缓存容量。虽然这些都是细节问题,但你在设计架构方案时都需要考虑清楚。
————————————————————————————————————————————————————————————————————————————————————
第四讲:流量削峰这是该怎么做?(https://time.geekbang.org/column/article/40736)
一个是通过队列来缓冲请求,即控制请求的发出;一个是通过答题来延长请求发出的时间,在请求发出后承接请求时进行控制,最后再对不符合条件的请求进行过滤;最后一种是对请求进行分层过滤。

削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。针对秒杀这一场景,削峰从本质上来说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求,它遵从“请求数要尽量少”的原则。

无损削峰的一般思路:排队 答题 分层过略

排队:把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。

答题:目的一防止部分买家使用秒杀器作弊;目的二延缓请求,起到对请求流量进行削峰的目的从而更好的支持瞬时流量高峰
答题设计思路:

题库生成模块:这个部分主要就是生成一个个问题和答案,其实题目和答案本身并不需要很复杂,重要的是能够防止由机器来算出结果,即防止秒杀器来答题。
题库的推送模块:用于在秒杀答题前,把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的,目的也是防止答题作弊。
题目的图片生成模块:用于把题目生成为图片格式,并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题,它要求只有人才能理解题目本身的含义。这里还要注意一点,由于答题时网络比较拥挤,我们应该把题目的图片提前推送到 CDN 上并且要进行预热,不然的话当用户真正请求题目时,图片可能加载比较慢,从而影响答题的体验。

库存校验设计图:

分层过滤:

——————————————————————————————————————————————————————————————————————————————————————
第五讲:影响性能的因素有哪些?又该如何提高系统的性能?(https://time.geekbang.org/column/article/40742)
CPU 主要看主频、磁盘主要看 IOPS(Input/Output Operations Per Second,即每秒进行读写操作的次数);
系统服务端性能,一般用 QPS(Query Per Second,每秒请求数)来衡量,还有一个影响和 QPS 也息息相关,那就是响应时间(Response Time,RT),它可以理解为服务器处理响应的耗时。

理论上就变成了“总 QPS =(1000ms / 响应时间)× 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。

1.响应时间对QPS的影响
真正对性能有影响的是 CPU 的执行时间。因为 CPU 的执行真正消耗了服务器的资源。经过实际的测试,如果减少 CPU 一半的执行时间,就可以增加一倍的 QPS。所以要想减少响应时间应该致力于减少CPU的执行时间。
2.线程数对QPS的影响

秒杀场景主要性能瓶颈在CPU,最常用的就是 JProfiler 和 Yourkit 这两个工具,它们可以列出整个请求中每个函数的 CPU 执行时间,可以发现哪个函数消耗的 CPU 时间最多。

怎样简单地判断 CPU 是不是瓶颈呢?一个办法就是看当 QPS 达到极限时,你的服务器的 CPU 使用率是不是超过了 95%,如果没有超过,那么表示 CPU 还有提升的空间,要么是有锁限制,要么是有过多的本地 I/O 等待发生。

Java系统优化方法:
减少编码:

减少序列化:

java极致优化:

并发读优化:

一般思路:
1.发现短板=》减少数据(服务端的数据处理、网络传输数据)=》数据分级(分清主次)=》减少中间环节,减少字符到字节的转换,增加预处理(提前做好字符到字节的转换),去掉不需要的操作=》做好优化

补充:
性能优化的核心就一个字-减
如:
1:异步化-减少等待响应的时间
2:降日志-减本地磁盘的交互
3:多级缓存-再减少获取数据路径
4:减功能-非核心功能或后补功能去掉

——————————————————————————————————————————————————————————————————————————————————
第六讲:秒杀系统“减库存”设计的核心逻辑https://time.geekbang.org/column/article/40743
减库存的方式:
下单减库存:
即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。(不足之处在于可能存在恶意下单导致正常用户无法或者难以正常秒杀)
付款减库存:
即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。(不足之处在于可能库存超卖,假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。)
预扣库存:下单时先预扣,在规定时间内不付款再释放库存
这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。(这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢?其实没有!针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。)

秒杀减库存的极致优化:

补充:
下单和扣库存两个操作的事务性是怎么做的?
可以分两步来做,先创建订单但是先不生效,然后减库存,如果减库存成功后再生效订单,否则订单不生效
————————————————————————————————————————————————————————————————————————————————————
第七讲:准备Plan B:如何设计兜底方案?https://time.geekbang.org/column/article/40744

高可用建设涉及方面:

高并发的秒杀系统兜底方案:降级、限流和拒绝服务。

降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措。

(客户端和服务端限流是针对rpc调用来说的,发起方可以理解为客户端,调用方可以理解为服务端,限流就是分别限制发起方和调用方的次数)
在限流的实现手段上来讲,基于 QPS 和线程数的限流应用最多,最大 QPS 很容易通过压测提前获取,例如我们的系统最高支持 1w QPS 时,可以设置 8000 来进行限流保护。线程数限流在客户端比较有效,例如在远程调用时我们设置连接池的线程数,超出这个并发线程请求,就将线程进行排队或者直接超时丢弃。

限流无疑会影响用户的正常请求,所以必然会导致一部分用户请求失败,因此在系统处理这种异常时一定要设置超时时间,防止因被限流的请求不能 fast fail(快速失败)而拖垮系统。

——————————————————————————————————————————————————————————————————————————————————————
第八讲:答疑解惑https://time.geekbang.org/column/article/68247

  1. 应用层排队的问题,应用层用队列接受请求,然后结果怎么返回的问题。
    此处的排队,更多地是说在服务端的服务调用之间采用排队的策略。例如,秒杀需要调用商品服务、调用价格优惠服务或者是创建订单服务,由于调用这些服务出现性能瓶颈,或者由于热点请求过于集中导致远程调用的连接数都被热点请求占据,那么那些正常的商品请求(非秒杀商品)就得不到服务器的资源了,这样对整个网站来说是不公平的。
    通常的解决方案就是在部分服务调用的地方对请求进行 Hash 分组,来限制一部分热点请求过多地占用服务器资源,分组的策略就可以根据商品 ID 来进行 Hash,热点商品的请求始终会进入一个分组中。
    结果的返回没必要太关注,因为服务端接受请求本身就是按照请求顺序处理的,而且这个处理在 Web 层是实时同步的,处理的结果也会立马就返回给用户。但是整个请求的处理涉及很多服务调用也涉及很多其他的系统,也会有部分的处理需要排队,所以可能有部分先到的请求由于后面的一些排队的服务拖慢,导致最终整个请求处理完成的时间反而比较后面的请求慢的情况。

采用请求对列方式的问题及做法:
问题:
一是体验会比较差,因为是异步的方式,在页面中搞个倒计时,处理的时间会长一点;
二是如果是根据入队列的时间来判断谁获得秒杀商品,那也太没有意思了,没有运气成分不也就没有惊喜
做法:
一是页面中采用轮询的方式定时主动去服务端查询结果,例如每秒请求一次服务端看看有没有处理结果(现在很多支付页面都采用了这种策略),这种方式的缺点是服务端的请求数会增加不少。
二是采用主动 push 的方式,这种就要求服务端和客户端保持连接了,服务端处理完请求主动 push 给客户端,这种方式的缺点是服务端的连接数会比较多。

2.一个商品数据存储在多个 Cache 实例中,如何保证数据一致性
Hash 分组可以基于 Nginx+Varnish 实现的,Nginx 把请求的 URL 中的商品 ID 进行 Hash 并路由到一个 upstream 中,这个 upstream 挂载一个 Varnish 分组(如下图所示)。这样,一个相同的商品就可以随机访问一个分组的任意一台 Varnish 机器了。


3.Cache失效问题:
为了保证数据的一致性,所以必然存在失效。
失效方式:
被动失效,主要处理如模板变更和一些对时效性不太敏感数据的失效,采用设置一定时间长度(如只缓存 3 秒钟)这种自动失效的方式。当然,你也要开发一个后台管理界面,以便能够在紧急情况下手工失效某些 Cache。
主动失效,一般有 Cache 失效中心监控数据库表变化发送失效请求、系统发布也需要清空 Cache 数据等几种场景。其中失效中心承担了主要的失效功能,这个失效中心的逻辑图如下:

失效中心会监控关键数据表的变更(有个中间件来解析 MySQL 的 binglog,然后发现有 Insert、Update、Delete 等操作时,会把变更前的数据以及要变更的数据转成一个消息发送给订阅方),通过这种方式来发送失效请求给 Cache,从而清除 Cache 数据。如果 Cache 数据放在 CDN 上,那么也可以采用类似的方式来设计级联的失效结构,采用主动发请求给 Cache 软件失效的方式,如下图所示:

posted on 2018-12-17 20:31  Ryanyanglibin  阅读(1494)  评论(0编辑  收藏  举报

导航