秒杀系统----大并发、高性能、高可用系统的设计学习
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>一、秒杀系统架构设计的关键点</h2>
<p>2019-10-19 周六</p>
<p>秒杀就是在同一个时刻有大量的请求争抢购买同一个商品并完成交易的过程,用技术的行话来说就是大量的并发读和并发写。</p>
<p>必要条件:支撑百万级的请求流量 && 高并发下数据的一致性写。</p>
<h3>1. 秒杀其实主要解决两个问题,一个是并发读,一个是并发写。</h3>
<pre><code>并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。</code></pre>
<p>要想打造并维护一个超大流量并发读写、高性能、高可用的系统,在整个用户请求路径上、从浏览器到服务端我们要遵循以下几个原则:</p>
<ul>
<li>要保证用户请求的数据尽量少</li>
<li>请求数尽量少</li>
<li>路径尽量短</li>
<li>依赖尽量少</li>
<li>不要有单点</li>
</ul>
<h3>2. 秒杀的整体架构可以概括为“稳、准、快”几个关键字</h3>
<h4>a.『稳』---- 高性能</h4>
<p>秒杀涉及大量的并发读和并发写,支持高并发访问非常关键。</p>
<ul>
<li>设计数据的动静分离方案</li>
<li>热点的发现与隔离</li>
<li>请求的削峰与分层过滤</li>
<li>服务端的极致优化</li>
</ul>
<pre><code>整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子。要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这是最基本的前提。</code></pre>
<h4>b. 『准』---- 一致性</h4>
<p>要求保证数据的一致性,库存数据既不能多也不能少</p>
<pre><code>秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。</code></pre>
<h4>c. 『快』---- 高可用</h4>
<p>系统的性能要足够高,能够支撑起大流量访问 && PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。</p>
<pre><code>不仅是服务端要做极致的性能优化,在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就趋于完美。</code></pre>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>二、秒杀系统设计时的5个架构原则</h2>
<p>2019-10-20 周日</p>
<p>秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。</p>
<h3>1. 架构原则:『4要1不要』</h3>
<h4>a. 数据要尽量少</h4>
<ul>
<li>
<p>首先是指用户请求的数据能少就少
【请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)】</p>
<pre><code>因为首先这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。例如,可以简化页面的大小,去掉不必要的页面装修效果等等。</code></pre>
</li>
<li>其次,“数据要尽量少”还要求系统依赖的数据能少就少
【包括系统完成某些业务逻辑需要读取和保存的数据----一般和后台服务及数据库打交道】
<pre><code>调用其他服务会涉及数据的序列化和反序列化,而这也是 CPU 的一大杀手,同样也会增加延时。而且,数据库本身也容易成为一个瓶颈,所以和数据库打交道越少越好,数据越简单、越小则越好。</code></pre></li>
</ul>
<h4>b. 请求数要尽量少</h4>
<ul>
<li>
<p>用户请求的页面返回后,浏览器渲染当前页面还要包含其他的额外请求。</p>
<pre><code>当前页面依赖的 CSS/JavaScript、图片,以及 Ajax 等</code></pre>
</li>
<li>因为浏览器每发出一个请求都多少会有一些消耗。
<pre><code>例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。</code></pre>
<h6>结论:减少请求数可以显著减少上述因素导致的资源消耗。</h6>
<pre><code>减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件,把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开。这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个 URL,然后动态把这些文件合并起来一起返回。</code></pre></li>
</ul>
<h5>c. 路径要尽量短</h5>
<ul>
<li>所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。通常,这些节点可以表示为一个系统或者一个新的 Socket 连接。
<pre><code>每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的话,那么整个请求的可用性是:99.9% 的 5 次方,约等于 99.5%。</code></pre></li>
<li>
<h5>缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。</h5>
<pre><code>缩短访问路径有一种办法-----多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用。</code></pre>
</li>
</ul>
<h4>d. 依赖要尽量少</h4>
<ul>
<li>
<p>依赖指的是要完成一次用户请求必须依赖的系统或者服务。</p>
<pre><code>假设要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。</code></pre>
</li>
<li>要减少依赖,可以给系统进行分级。
比如 0 级系统、1 级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,那么 0 级系统强依赖的系统也同样是最重要的系统,以此类推。
<pre><code>级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。</code></pre></li>
</ul>
<h4>e. 不要有单点</h4>
<ul>
<li>
<p>单点意味着没有备份,风险不可控。
【设计分布式系统最重要的原则就是“消除单点”】</p>
</li>
<li>
<p>如何避免单点?
【应用服务无状态化----避免将服务的状态和机器绑定】</p>
</li>
<li>
<p>服务的状态和机器如何解耦?
【把和机器相关的配置动态化】</p>
<pre><code>nacos ---- 这些参数可以通过配置中心来动态推送,在服务启动时动态拉取下来,我们在这些配置中心设置一些规则来方便地改变这些映射关系。</code></pre>
</li>
<li>数据存储如何解决单点问题?
【存储服务本身很难无状态化,因为数据要存储在磁盘上,本身就要和机器绑定,该场景下一般要通过冗余多个备份的方式来解决单点问题】</li>
</ul>
<h4>不是请求最少就一定最好</h4>
<ul>
<li>
<h5>架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。</h5>
<pre><code>例如把有些 CSS 内联进页面里,这样做可以减少依赖一个 CSS 的请求从而加快首页的渲染,但是同样也增大了页面的大小,又不符合“数据要尽量少”的原则。这种情况下我们为了提升首屏的渲染速度,只把首页的 HTML 依赖的 CSS 内联进来,其他 CSS 仍然放到文件中作为依赖加载,尽量实现首页的打开速度与整个页面加载性能的平衡。</code></pre>
</li>
</ul>
<h3>2. 不同场景下的不同架构案例</h3>
<h4>a. 最简单的秒杀系统</h4>
<p>商品购买页面增加一个“定时上架”功能,仅在秒杀开始时才让用户看到购买按钮,当商品的库存卖完了也就结束了。</p>
<h4>b. 架构改造,设计新的秒杀系统</h4>
<p>秒杀详情成为一个独立的新系统,另外核心的数据放到缓存(Cache)中,其他的关联系统也都以独立集群的方式进行部署。
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/6923cdb293aa14f5624dcf42bf8b57d7?showdoc=.jpg" alt="" /></p>
<ul>
<li>
<p>把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化,例如这个独立出来的系统就减少了店铺装修的功能,减少了页面的复杂度;</p>
</li>
<li>
<p>在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;</p>
</li>
<li>
<p>将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;</p>
</li>
<li>增加秒杀答题,防止有秒杀器抢单。</li>
</ul>
<h4>c. 进一步提升系统性能,支持高并发请求</h4>
<p>对页面进一步的静态化,秒杀过程中不需要刷新整个页面,而只需要向服务端请求很少的动态数据。最关键的详情和交易系统都增加了本地缓存,来提前缓存秒杀商品的信息,热点数据库也做了独立部署。
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/1cb0682aa6b1c190aaf5e0191f42dcf2?showdoc=.jpg" alt="" /></p>
<ul>
<li>
<p>对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;</p>
</li>
<li>
<p>在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。</p>
</li>
<li>增加系统限流保护,防止最坏情况发生。</li>
</ul>
<h5>痛点:越追求极致性能,系统定制开发越多,导致系统的通用性越差。</h5>
<pre><code>例如,把秒杀商品缓存在每台机器的内存中,这种方式显然不适合太多的商品同时进行秒杀的情况,因为单机的内存始终有限。所以要取得极致的性能,就要在其他地方(比如,通用性、易用性、成本等方面)有所牺牲。</code></pre>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>三、数据的动静分离</h2>
<p>2019-10-26 周六</p>
<p>系统如何才能『快』?(<strong>动静分离的方向</strong>)</p>
<ul>
<li>
<p>一点是提高单次请求的效率;</p>
</li>
<li>一点是减少没必要的请求</li>
</ul>
<h3>1. 动静分离</h3>
<p>把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。</p>
<h5>“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。</h5>
<pre><code>很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。</code></pre>
<pre><code>现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据。</code></pre>
<ul>
<li>
<p>静态数据,不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面,它也可能是经过 Java 系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。</p>
</li>
<li>页面中“不包含”,指的是“页面的 HTML 源码中不含有”。</li>
</ul>
<h5>分离了动静数据,就可以对分离出来的静态数据做缓存。有了缓存之后,就提高了静态数据的“访问效率”。</h5>
<h3>2. 如何对静态数据区做缓存?</h3>
<h4>a. 静态数据缓存到离用户最近的地方</h4>
<pre><code>静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。常见的就三种,用户浏览器里、CDN 上或者在服务端的 Cache 中。应该根据情况,把它们尽量缓存到离用户最近的地方。</code></pre>
<h4>b. 静态化改造 ---- 直接缓存 HTTP 连接</h4>
<p>相较于普通的数据缓存而言,还有一种方式是系统的静态化改造。
静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据。如下图所示,Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。</p>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/59760267dd41eeac357245a9e07eac86?showdoc=.jpg" alt="" /></p>
<h4>c. 让谁来缓存静态数据也很重要</h4>
<pre><code>不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例,因为 Java 系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet 容器解析 HTTP 协议较慢),所以你可以不在 Java 层做缓存,而是直接在 Web 服务器层上做,这样你就可以屏蔽 Java 语言层面的一些弱点;而相比起来,Web 服务器(如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。</code></pre>
<h3>3. 如何做动静分离的改造</h3>
<h4>a. 做动静分离,分离出动态内容</h4>
<p>以典型的商品详情系统为例来详细介绍。</p>
<ul>
<li>
<p>URL 唯一化。</p>
<pre><code>商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分。</code></pre>
</li>
<li>
<p>分离浏览者相关的因素。</p>
<pre><code>浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。</code></pre>
</li>
<li>
<p>分离时间因素。</p>
<pre><code>服务端输出的时间也通过动态请求获取。</code></pre>
</li>
<li>
<p>异步化地域因素。</p>
<pre><code>详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。</code></pre>
</li>
<li>去掉 Cookie。
<pre><code>服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。</code></pre></li>
</ul>
<h4>b. 动态内容的处理</h4>
<ul>
<li>
<p>ESI(Edge Side Includes)方案</p>
<pre><code>在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。</code></pre>
</li>
<li>CSI(Client Side Include)方案
<pre><code>单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。</code></pre></li>
</ul>
<h3>4. 动静分离的架构方案</h3>
<p>上述通过改造将静态数据区和动态数据做了分离,但是想要完整的输出给用户,还需要在系统架构上进一步对这些动态和静态数据区重新组合。</p>
<h4>a. 实体机单机部署</h4>
<p>这种方案是将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。</p>
<p>这里将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。</p>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/3284f0460b665230ed8b8ffabb8ee262?showdoc=.jpg" alt="" /></p>
<h5>实体机单机部署的优点</h5>
<ul>
<li>
<p>没有网络瓶颈,而且能使用大内存;</p>
</li>
<li>
<p>既能提升命中率,又能减少 Gzip 压缩;</p>
</li>
<li>减少 Cache 失效压力,因为采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效。</li>
</ul>
<p>这个方案中,虽然把通常只需要虚拟机或者容器运行的 Java 应用换成实体机,优势很明显,它会增加单机的内存容量,但是一定程度上也造成了 CPU 的浪费,因为单个的 Java 进程很难用完整个实体机的 CPU。</p>
<p>另外就是,一个实体机上部署了 Java 应用又作为 Cache 来使用,这造成了运维上的高复杂度,所以这是一个折中的方案。</p>
<p>如果有多个业务系统都有静态化改造的需求,建议把 Cache 层单独抽出来公用比较合理。</p>
<h4>b. 统一 Cache 层</h4>
<p>是将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。统一 Cache 层是个更理想的可推广方案。</p>
<p>将 Cache 层单独拿出来统一管理可以减少运维成本,同时也方便接入其他静态化系统。</p>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/54815d828c25aedc31d18d9002c312ba?showdoc=.jpg" alt="" /></p>
<h5>统一 Cache 层的优点</h5>
<ul>
<li>
<p>单独一个 Cache 层,可以减少多个应用接入时使用 Cache 的成本。这样接入的应用只要维护自己的 Java 系统就好,不需要单独维护 Cache,而只关心如何使用即可。</p>
</li>
<li>
<p>统一 Cache 的方案更易于维护,如后面加强监控、配置的自动化,只需要一套解决方案就行,统一起来维护升级也比较方便。</p>
</li>
<li>可以共享内存,最大化利用内存,不同系统之间的内存可以动态切换,从而能够有效应对各种攻击。</li>
</ul>
<h5>缺点:缓存更加集中,导致:</h5>
<ul>
<li>
<p>Cache 层内部交换网络成为瓶颈;</p>
</li>
<li>
<p>缓存服务器的网卡也会是瓶颈;</p>
</li>
<li>机器少风险较大,挂掉一台就会影响很大一部分缓存数据。</li>
</ul>
<p>要解决上面这些问题,可以再对 Cache 做 Hash 分组,即一组 Cache 缓存的内容相同,这样能够避免热点数据过度集中导致新的瓶颈产生。</p>
<h4>c. 上CDN</h4>
<p>将 Cache 进一步前移到 CDN 上,因为 CDN 离用户最近,效果会更好。</p>
<h5>CDN的问题</h5>
<ul>
<li>
<p>失效问题(缓存时效)
需要保证 CDN 可以在秒级时间内,让分布在全国各地的 Cache 同时失效,这对 CDN 的失效系统要求很高。</p>
<pre><code>静态数据是『可能会变化的』。
比如一篇文章,现在不变,但如果你发现个错别字,是不是就会变化了?如果你的缓存时效很长,那用户端在很长一段时间内看到的都是错的。</code></pre>
</li>
<li>
<p>命中率问题
Cache 最重要的一个衡量指标就是“高命中率”,不然 Cache 的存在就失去了意义。</p>
<pre><code>如果将数据全部放到全国的 CDN 上,必然导致 Cache 分散,而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低,那么命中率就成为一个问题。</code></pre>
</li>
<li>发布更新问题
如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,而且还要考虑有问题时快速回滚和排查问题的简便性。</li>
</ul>
<p>从前面的分析来看,将商品详情系统放到全国的所有 CDN 节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件:</p>
<ul>
<li>
<p>靠近访问量比较集中的地区;</p>
</li>
<li>
<p>离主站相对较远;</p>
</li>
<li>
<p>节点到主站间的网络比较好,而且稳定;</p>
</li>
<li>
<p>节点容量比较大,不会占用其他 CDN 太多的资源;</p>
</li>
<li>节点不要太多。</li>
</ul>
<h5>基于上面几个因素,选择 CDN 的二级 Cache 比较合适,因为二级 Cache 数量偏少,容量也更大,让用户的请求先回源的 CDN 的二级 Cache 中,如果没命中再回源站获取数据,部署方式如下所示</h5>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/1944d5b6b2733fd681e53aa0899b3d5f?showdoc=.jpg" alt="" /></p>
<p>使用 CDN 的二级 Cache 作为缓存,可以达到和当前服务端静态化 Cache 类似的命中率,因为节点数不多,Cache 不是很分散,访问量也比较集中,这样也就解决了命中率问题,同时能够给用户最好的访问体验,是当前比较理想的一种 CDN 化方案。</p>
<h5>CDN 化部署方案的特点:</h5>
<pre><code>1. 把整个页面缓存在用户浏览器中;
2. 如果强制刷新整个页面,也会请求 CDN;
3. 实际有效请求,只是用户对“刷新抢宝”按钮的点击。</code></pre>
<p>把 90% 的静态数据缓存在了用户端或者 CDN 上,当真正秒杀时,用户只需要点击特殊的“刷新抢宝”按钮,而不需要刷新整个页面。</p>
<p>这样一来,系统只是向服务端请求很少的有效数据,而不需要重复请求大量的静态数据。秒杀的动态数据和普通详情页面的动态数据相比更少,性能也提升了 3 倍以上。</p>
<h3>5. 存储在浏览器或 CDN 上的区别</h3>
<p>在 CDN 上缓存可以做主动失效,而在用户的浏览器里就更不可控,如果用户不主动刷新的话,很难主动地把消息推送给用户的浏览器。</p>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>四、热点数据发现与隔离</h2>
<p>2019-11-02 周六</p>
<h3>1. 为什么关注热点?</h3>
<p>热点会对系统产生一系列的影响。</p>
<p>首先,热点请求会大量占用服务器处理资源,虽然这个热点可能只占请求总量的亿分之一,然而却可能抢占 90% 的服务器资源,如果这个热点请求还是没有价值的无效请求,那么对系统资源来说完全是浪费。</p>
<p>其次,即使这些热点是有效的请求,也要将其识别出来做针对性的优化,从而用更低的代价来支撑这些热点请求。</p>
<h3>2. 什么是热点?</h3>
<p>热点分为热点操作和热点数据。</p>
<ul>
<li>
<h4>热点操作</h4>
<pre><code>例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单等都属于此类操作。对系统来说,这些操作可以抽象为“读请求”和“写请求”,这两种热点请求的处理方式大相径庭,读请求的优化空间要大一些,而写请求的瓶颈一般都在存储层,优化的思路就是根据 CAP 理论做平衡。</code></pre>
</li>
<li>
<h4>热点数据</h4>
<p>用户的热点请求对应的数据,又分为“静态热点数据”和“动态热点数据”。
“静态热点数据”,就是能够提前预测的热点数据;</p>
<pre><code>例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。</code></pre>
<p>“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点</p>
<pre><code>卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。</code></pre>
</li>
</ul>
<h3>3. 如何发现热点数据?</h3>
<ul>
<li>
<h4>发现静态热点数据</h4>
<pre><code>静态热点数据可以通过商业手段,例如强制让卖家通过报名参加的方式提前把热点商品筛选出来,实现方式是通过一个运营系统,把参加活动的商品数据进行打标,然后通过一个后台系统对这些热点商品进行预处理,如提前进行缓存。但是这种通过报名提前筛选的方式也会带来新的问题,即增加卖家的使用成本,而且实时性较差,也不太灵活。</code></pre>
<pre><code>还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,我们可以认为这些 TOP N 的商品就是热点商品。</code></pre>
</li>
<li>
<h4>发现动态热点数据</h4>
<pre><code>可以通过卖家报名或者大数据预测这些手段来提前预测静态热点数据,但这其中有一个痛点,就是实时性较差。</code></pre>
<h5>设计一个动态热点发现系统</h5>
<pre><code>a. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。</code></pre>
<pre><code>b. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL。</code></pre>
<pre><code>c. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。</code></pre>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/d37f6f7b54a3c66fe202ee7a6abe55f0?showdoc=.jpg" alt="" />
用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。</p>
<pre><code>通过部署在每台机器上的 Agent 把日志汇总到聚合和分析集群中,然后把符合一定规则的热点数据,通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到 Cache 中,或者直接推送到应用服务器的内存中,还可以对这些数据进行拦截,总之下游系统可以订阅这些数据,然后根据自己的需求决定如何处理这些数据。</code></pre>
<h4>注意事项</h4>
<pre><code>1.热点服务后台抓取热点数据日志最好采用异步方式,因为“异步”一方面便于保证通用性,另一方面又不影响业务系统和中间件产品的主流程。</code></pre>
<pre><code>2.热点服务发现和中间件自身的热点保护模块并存,每个中间件和应用还需要保护自己。热点服务台提供热点数据的收集和订阅服务,便于把各个系统的热点数据透明出来。</code></pre>
<pre><code>3.热点发现要做到接近实时(3s 内完成热点数据的发现),因为只有做到接近实时,动态发现才有意义,才能实时地对下游系统提供保护。</code></pre>
</li>
</ul>
<h3>4. 如何让处理热点数据?</h3>
<ul>
<li>
<h4>优化</h4>
<p>优化热点数据最有效的办法就是缓存热点数据。</p>
<pre><code>如果热点数据做了动静分离,那么可以长期缓存静态数据。但是,缓存热点数据更多的是“临时”缓存,即不管是静态数据还是动态数据,都用一个队列短暂地缓存数秒钟,由于队列长度有限,可以采用 LRU 淘汰算法替换。</code></pre>
</li>
<li>
<h4>限制</h4>
<p>限制更多的是一种保护机制。</p>
<pre><code>限制的办法也有很多,例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。</code></pre>
</li>
<li>
<h4>隔离</h4>
<p>将热点数据隔离出来,不要让 1% 的请求影响到另外的 99%,隔离出来后也更方便对这 1% 的请求做针对性的优化。</p>
<pre><code>1.业务隔离
把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。</code></pre>
<pre><code>2.系统隔离。
系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99% 分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。</code></pre>
<pre><code>3.数据隔离。
秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01% 的数据有机会影响 99.99% 数据。</code></pre>
</li>
</ul>
<h3>5. 小结</h3>
<ul>
<li>
<p>a. 『热点发现与隔离』和 『数据的动静分离』不一样,从另外一个维度对数据进行了区分处理。区分的目的主要还是对读热点数据加以优化,对照“4 要 1 不要”原则,它可以减少请求量,也可以减少请求的路径。因为缓存的数据都是经过多个请求,或者从多个系统中获取的数据经过计算后的结果。</p>
</li>
<li>
<p>b. 热点的发现和隔离对其他的高性能分布式系统也非常有价值,尤其是热点的隔离非常重要。最重要最简单的方式就是独立出来一个集群,单独处理热点数据。</p>
</li>
<li>c. 能够独立出来一个集群的前提还是首先能够发现热点,可以通过比如人工标识、大数据统计计算,以及实时热点发现方案等发现热点。</li>
</ul>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>五、流量削峰 && 分层过滤</h2>
<p>2019-11-05 周六</p>
<p>如果查看秒杀系统的流量监控图,在活动开始那一秒是一条很直很直的线,这是因为秒杀请求在时间上高度集中于某一特定的时间点。这会导致一个特别高的流量峰值,它<strong>对资源的消耗是瞬时</strong>的。</p>
<p>对秒杀这个场景来说,最终能够抢到商品的人数是固定的。也就是说 100 人和 10000 人发起请求的结果都是一样的,并发度越高,无效请求也越多。</p>
<p>从业务上来说,<strong>秒杀活动希望更多的人来参与</strong>。也就是开始之前希望有更多的人来刷页面,但是真正开始下单时,秒杀请求并不是越多越好。因此可以设计一些规则,让并发的请求更多地延缓,甚至可以过滤掉一些无效请求。</p>
<h3>1. 为什么要削峰?</h3>
<p>服务器的处理资源是恒定的,如果出现峰值的话,很容易导致忙到处理不过来;闲的时候却又没有什么要处理。但是由于要保证服务质量,我们的很多处理资源只能按照忙的时候来预估,而这会导致<strong>资源的浪费</strong>。</p>
<ul>
<li>
<h4>削峰可以让服务端处理变得更加平稳</h4>
</li>
<li>
<h4>削峰可以节省服务器的资源成本</h4>
</li>
</ul>
<h3>2. 削峰操作之排队</h3>
<p>把“一步的操作”变成“两步的操作”,其中增加的一步操作用来起到缓冲的作用。</p>
<ul>
<li>
<h4>a. 消息队列</h4>
<p>用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/ec2c8779c406ca2333255292cfb339c0?showdoc=.jpg" alt="" />
消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。</p>
<pre><code>如果流量峰值持续一段时间达到了消息队列的处理上限,例如本机的消息积压达到了存储空间的上限,消息队列同样也会被压垮,这样虽然保护了下游的系统,但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时,即使是有水库恐怕也无济于事。</code></pre>
</li>
<li>
<h4>b. 线程池加锁等待</h4>
</li>
<li>
<h4>c. 内存排队算法----先进先出、先进后出</h4>
</li>
<li>
<h4>d. 把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制)来恢复请求等方式。</h4>
</li>
</ul>
<h3>3. 削峰操作之答题</h3>
<h4>a. 增加操作的复杂度</h4>
<ul>
<li>
<h5>第一个目的是防止部分买家使用秒杀器在参加秒杀时作弊。</h5>
</li>
<li>
<h5>第二个目的其实就是延缓请求,起到对请求流量进行削峰的作用,从而让系统能够更好地支持瞬时的流量高峰。</h5>
<pre><code>把峰值的下单请求拉长,从以前的 1s 之内延长到 2s~10s。这样一来,请求峰值基于时间分片了。这个时间的分片对服务端处理并发非常重要,会大大减轻压力。而且,由于请求具有先后顺序,靠后的请求到来时自然也就没有库存了,因此根本到不了最后的下单步骤,所以真正的并发写就非常有限了。</code></pre>
</li>
</ul>
<h4>b. 答题设计思路</h4>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/68a35541c4c6b79549372c84a48d1ed7?showdoc=.jpg" alt="" /></p>
<ul>
<li>
<h5>题库生成模块</h5>
<p>这个部分主要就是生成一个个问题和答案,其实题目和答案本身并不需要很复杂,重要的是能够防止由机器来算出结果,即防止秒杀器来答题。</p>
</li>
<li>
<h5>题库的推送模块。</h5>
<p>用于在秒杀答题前,把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的,目的也是防止答题作弊。</p>
</li>
<li>
<h5>题目的图片生成模块。</h5>
<p>用于把题目生成为图片格式,并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题,它要求只有人才能理解题目本身的含义。这里还要注意一点,由于答题时网络比较拥挤,我们应该把题目的图片提前推送到 CDN 上并且要进行预热,不然的话当用户真正请求题目时,图片可能加载比较慢,从而影响答题的体验。</p>
</li>
</ul>
<pre><code>注1:除了验证问题的答案以外,还包括用户本身身份的验证,例如是否已经登录、用户的 Cookie 是否完整、用户是否重复频繁提交等。</code></pre>
<pre><code>注2:除了做正确性验证,我们还可以对提交答案的时间做些限制,例如从开始答题到接受答案要超过 1s,因为小于 1s 是人为操作的可能性很小,这样也能防止机器答题的情况。</code></pre>
<h3>4. 削峰操作之分层过滤</h3>
<h4>采用“漏斗”式设计对请求进行分层过滤,从而过滤掉一些无效的请求。</h4>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/4bc386e3ba19990b4bbdff4b35ae7285?showdoc=.jpg" alt="" />
假如请求分别经过 CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层,那么:
a. 大部分数据和流量在用户浏览器或者 CDN 上获取,这一层可以拦截大部分数据的读取;
b. 经过第二层(即前台系统)时数据(包括强一致性的数据)尽量得走 Cache,过滤一些无效的请求;
c. 再到第三层后台系统,主要做数据的二次检验,对系统做好保护和限流,这样数据量和请求就进一步减少;
d. 最后在数据层完成数据的强一致性校验。</p>
<h4>分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。</h4>
<ul>
<li>
<h5>分层校验的基本原则</h5>
<pre><code>1.将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;
2.对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
3.对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
4.对写请求做限流保护,将超出系统承载能力的请求过滤掉;
5.对写数据进行强一致性校验,只保留最后有效的数据。</code></pre>
</li>
<li>
<h5>分层校验的目的</h5>
<pre><code>在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。</code></pre>
</li>
</ul>
<h5>在削峰的处理方式上除了采用技术手段,其实还可以采用业务手段来达到一定效果,例如在零点开启大促的时候由于流量太大导致支付系统阻塞,这个时候可以采用发放优惠券、发起抽奖活动等方式,将一部分流量分散到其他地方,这样也能起到缓冲流量的作用。</h5>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>六、提高性能与系统优化</h2>
<p>2019-11-09 周六</p>
<h3>1. 系统服务端性能</h3>
<p>一般用 QPS(Query Per Second,每秒请求数)来衡量,还有一个影响和 QPS 也息息相关,那就是响应时间(Response Time,RT),它可以理解为服务器处理响应的耗时。</p>
<pre><code>正常情况下响应时间(RT)越短,一秒钟处理的请求数(QPS)自然也就会越多,这在单线程处理的情况下看起来是线性的关系,即我们只要把每个请求的响应时间降到最低,那么性能就会最高。</code></pre>
<pre><code>响应时间总有一个极限,不可能无限下降,所以又出现了另外一个维度,即通过多线程,来处理请求。这样理论上就变成了“总 QPS =(1000ms / 响应时间)× 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。</code></pre>
<ul>
<li>
<h4>响应时间和 QPS 的关系</h4>
<pre><code>响应时间一般都是由 CPU 执行时间和线程等待时间(比如 RPC、IO 等待、Sleep、Wait 等)组成,即服务器在处理一个请求时,一部分是 CPU 本身在做运算,还有一部分是在各种等待。(实际的测试发现,减少线程等待时间对提升性能的影响没有那么大,它并不是线性的提升关系)</code></pre>
<h5>真正对性能有影响的是 CPU 的执行时间-----减少 CPU 的执行时间</h5>
<p>因为 CPU 的执行真正消耗了服务器的资源。经过实际的测试,如果减少 CPU 一半的执行时间,就可以增加一倍的 QPS。</p>
</li>
<li>
<h4>线程数对 QPS 的影响</h4>
<h5>线程数不是越多越好,因为线程本身也消耗资源,也受到其他因素的制约。例如,线程越多系统的线程切换成本就会越高,而且每个线程也都会耗费一定内存。</h5>
<p>多线程的场景都有一个默认配置公式:</p>
<pre><code>“线程数 = 2 * CPU 核数 + 1”</code></pre>
<p>最佳实践所得公式:</p>
<pre><code>线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量</code></pre>
<h5>要提升性能我们就要减少 CPU 的执行时间,另外就是要设置一个合理的并发线程数,通过这两方面来显著提升服务器的性能。</h5>
</li>
</ul>
<h3>2. 如何发现瓶颈</h3>
<p>就服务器而言,会出现瓶颈的地方有很多,例如 CPU、内存、磁盘以及网络等都可能会导致瓶颈。此外,不同的系统对瓶颈的关注度也不一样,例如对缓存系统而言,制约它的是内存,而对存储型系统来说 I/O 更容易是瓶颈。</p>
<h5>秒杀场景的瓶颈更多地发生在 CPU 上。</h5>
<ul>
<li>
<h4>a. 如何发现 CPU 的瓶颈?</h4>
<pre><code>借助CPU 诊断工具: JProfiler 和 Yourkit</code></pre>
<pre><code>其次可以通过jstack 定时地打印调用栈。如果某些函数调用频繁或者耗时较多,那么那些函数就会多次出现在系统调用栈里,这样相当于采样的方式也能够发现耗时较多的函数。</code></pre>
</li>
<li>
<h4>b. 如何简单地判断 CPU 是不是瓶颈?</h4>
<pre><code>看当 QPS 达到极限时,你的服务器的 CPU 使用率是不是超过了 95%,如果没有超过,那么表示 CPU 还有提升的空间,要么是有锁限制,要么是有过多的本地 I/O 等待发生。</code></pre>
</li>
</ul>
<h3>3. 如何优化系统?(Java 系统为例)</h3>
<ul>
<li>
<h4>a. 减少编码</h4>
<p>每个字符的编码都需要查表,而这种查表的操作非常耗资源,所以减少字符到字节或者相反的转换、减少字符编码会非常有成效。减少编码就可以大大提升性能。</p>
<pre><code>Java 的编码运行比较慢,这是 Java 的一大硬伤。在很多场景下,只要涉及字符串的操作(如输入输出操作、I/O 操作)都比较耗 CPU 资源,不管它是磁盘 I/O 还是网络 I/O,因为都需要将字符转换成字节,而这个转换必须编码。</code></pre>
<p>把静态的字符串提前编码成字节并缓存,然后直接输出字节内容到页面。</p>
<pre><code>用 resp.getOutputStream() 函数写数据,把一些静态的数据提前转化成字节,等到真正往外写的时候再直接用 OutputStream() 函数写,就可以减少静态数据的编码转换。</code></pre>
</li>
<li>
<h4>减少序列化</h4>
<p>序列化也是 Java 性能的一大天敌,减少 Java 中的序列化操作也能大大提升性能。又因为序列化往往是和编码同时发生的,所以减少序列化也就减少了编码。</p>
<pre><code>序列化大部分是在 RPC 中发生的,因此避免或者减少 RPC 就可以减少序列化,当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案,就是可以将多个关联性比较强的应用进行“合并部署”,而减少不同应用之间的 RPC 也可以减少序列化的消耗。</code></pre>
<pre><code>“合并部署”,就是把两个原本在不同机器上的不同应用合并部署到一台机器上,当然不仅仅是部署在一台机器上,还要在同一个 Tomcat 容器中,且不能走本机的 Socket,这样才能避免序列化的产生。</code></pre>
</li>
<li>
<h4>Java 极致优化</h4>
<p>对大流量的 Web 系统做静态化改造,让大部分请求和数据直接在 Nginx 服务器或者 Web 代理服务器上直接返回(这样可以减少数据的序列化与反序列化),而 Java 层只需处理少量数据的动态请求。</p>
<pre><code>直接使用 Servlet 处理请求。避免使用传统的 MVC 框架,这样可以绕过一大堆复杂且用处不大的处理逻辑,节省 1ms 时间(具体取决于你对 MVC 框架的依赖程度)。</code></pre>
<pre><code>直接输出流数据。使用 resp.getOutputStream() 而不是 resp.getWriter() 函数,可以省掉一些不变字符数据的编码,从而提升性能;数据输出时推荐使用 JSON 而不是模板引擎(一般都是解释执行)来输出页面。</code></pre>
</li>
<li>
<h4>并发读优化</h4>
<p>采用应用层的 LocalCache,在秒杀系统的单机上缓存商品相关的数据,需要划分成动态数据和静态数据分别进行处理。</p>
<pre><code>像商品中的“标题”和“描述”这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;</code></pre>
<pre><code>像库存这类动态数据,会采用“被动失效”的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据</code></pre>
<p>读的场景可以允许一定的脏数据,因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存,可以等到真正写数据时再保证最终的一致性,通过在数据的高可用性和一致性之间的平衡,来解决高并发的数据读取问题。</p>
</li>
</ul>
<h3>4. 性能优化的过程</h3>
<ul>
<li>
<h4>发现短板</h4>
<pre><code>比如考虑以下因素的一些限制:光速(光速:C = 30 万千米 / 秒;光纤:V = C/1.5=20 万千米 / 秒,即数据传输是有物理距离的限制的)、网速(2017 年 11 月知名测速网站 Ookla 发布报告,全国平均上网带宽达到 61.24 Mbps,千兆带宽下 10KB 数据的极限 QPS 为 1.25 万 QPS=1000Mbps/8/10KB)、网络结构(交换机 / 网卡的限制)、TCP/IP、虚拟机(内存 /CPU/IO 等资源的限制)和应用本身的一些瓶颈等。</code></pre>
</li>
<li>
<h4>减少数据</h4>
<pre><code>有两个地方特别影响性能,一是服务端在处理数据时不可避免地存在字符到字节的相互转化,二是 HTTP 请求时要做 Gzip 压缩,还有网络传输的耗时,这些都和数据大小密切相关。</code></pre>
</li>
<li>
<h4>数据分级</h4>
<pre><code>要保证首屏为先、重要信息为先,次要信息则异步加载,以这种方式提升用户获取数据的体验。</code></pre>
</li>
<li>
<h4>减少中间环节</h4>
<pre><code>减少字符到字节的转换,增加预处理(提前做字符到字节的转换)去掉不需要的操作。</code></pre>
</li>
</ul>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>七、『减库存』设计</h2>
<p>2019-11-16 周六</p>
<h3>1. 减库存的几种方式</h3>
<ul>
<li>
<h4>a. 下单减库存</h4>
<pre><code>当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。</code></pre>
</li>
<li>
<h4>b. 付款减库存</h4>
<pre><code>买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。</code></pre>
</li>
<li>
<h4>c. 预扣库存</h4>
<pre><code>买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。</code></pre>
</li>
</ul>
<h3>2. 减库存存在的问题</h3>
<p>由于购物过程中存在两步或者多步的操作,因此在不同的操作步骤中减库存,就会存在一些可能被恶意买家利用的漏洞,例如发生恶意下单的情况。</p>
<ul>
<li>
<h4>a. 下单减库存</h4>
<pre><code>用户下单后就减去库存,正常情况下,买家下单后付款的概率会很高,所以不会有太大问题。但是有一种场景例外,就是当卖家参加某个活动时,此时活动的有效时间是商品的黄金售卖时间,如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单,让这款商品的库存减为零,那么这款商品就不能正常售卖了。</code></pre>
</li>
<li>
<h4>b. 付款减库存----库存超卖</h4>
<pre><code>假如有 100 件商品,就可能出现 300 人下单成功的情况,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。</code></pre>
</li>
<li>
<h4>c. 预扣库存</h4>
<p>在一定程度上缓解上面的问题,但不能彻底解决。</p>
<pre><code>针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件的方式把库存减完。针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。</code></pre>
<pre><code>针对“库存超卖”这种情况,在 10 分钟时间内下单的数量仍然有可能超过库存数量,遇到这种情况我们只能区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。</code></pre>
<h3>3. 秒杀系统如何减库存?</h3>
<p>由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。</p>
<pre><code>“下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错。</code></pre>
</li>
</ul>
<h3>4. 秒杀减库存的极致优化</h3>
<ul>
<li>
<p>a. 秒杀中并不需要对库存有精确的一致性读,把库存数据放到缓存(Cache)中,可以大大提升读性能。</p>
<pre><code>解决大并发读问题,可以采用 LocalCache(即在秒杀系统的单机上缓存商品相关的数据)和对数据进行分层过滤的方式,但是像减库存这种大并发写无论如何还是避免不了,这也是秒杀场景下最为核心的一个技术难题。</code></pre>
</li>
<li>
<p>b. 如果你的秒杀商品的减库存逻辑非常单一,比如没有复杂的 SKU 库存和总库存这种联动关系的话,可以把秒杀商品减库存直接放到带有持久化功能的缓存系统(如 Redis)中。</p>
</li>
<li>c. 单个热点商品会影响整个数据库的性能, 导致 0.01% 的商品影响 99.99% 的商品的售卖,这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行隔离,把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦,比如要做热点数据的动态迁移以及单独的数据库等。</li>
</ul>
<h3>5. 下单和扣库存两个操作的事务性操作</h3>
<pre><code>可以分两步来做,先创建订单但是先不生效,然后减库存,如果减库存成功后再生效订单,否则订单不生效</code></pre>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>七、降级 && 限流 && 拒绝服务</h2>
<p>2019-11-23 周六</p>
<h3>1. 高可用系统建设</h3>
<p><img src="https://www.showdoc.cc/server/api/common/visitfile/sign/006f9cc358b7709a558f9d4673a01b29?showdoc=.jpg" alt="" /></p>
<ul>
<li>
<h4>架构阶段</h4>
<pre><code>架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站的运转。</code></pre>
</li>
<li>
<h4>编码阶段</h4>
<pre><code>编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果。</code></pre>
</li>
<li>
<h4>测试阶段</h4>
<pre><code>测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的处理流程。</code></pre>
</li>
<li>
<h4>发布阶段</h4>
<pre><code>发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急的回滚机制。</code></pre>
</li>
<li>
<h4>运行阶段</h4>
<pre><code>运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。</code></pre>
</li>
<li>
<h4>故障发生</h4>
<pre><code>故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够及时恢复服务,并定位原因解决问题。</code></pre>
</li>
</ul>
<h3>2. 降级</h3>
<p>当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。</p>
<pre><code>它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。</code></pre>
<ul>
<li>
<h4>开关系统</h4>
<p>一部分是开关控制台,它保存了开关的具体配置信息,以及具体执行开关所对应的机器列表;另一部分是执行下发开关数据的 Agent,主要任务就是保证开关被正确执行,即使系统重启后也会生效。
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/3a9a324284a46b7c296707a01d98cfff?showdoc=.jpg" alt="" /></p>
<pre><code>执行降级无疑是在系统性能和用户体验之间选择了前者,降级后肯定会影响一部分用户的体验,例如在双 11 零点时,如果优惠券系统扛不住,可能会临时降级商品详情的优惠信息展示,把有限的系统资源用在保障交易系统正确展示优惠信息上,即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措。</code></pre>
</li>
</ul>
<h3>3. 限流</h3>
<p>限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/6db9c266419655261eaab686b001dbac?showdoc=.jpg" alt="" />
限流既可以是在客户端限流,也可以是在服务端限流。
限流的实现方式既要支持 URL 以及方法级别的限流,也要支持基于 QPS 和线程的限流。</p>
<h4>客户端限流</h4>
<pre><code>好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。</code></pre>
<h4>服务端限流</h4>
<pre><code>好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。</code></pre>
<p>在限流的实现手段上来讲,基于 QPS 和线程数的限流应用最多,最大 QPS 很容易通过压测提前获取,例如我们的系统最高支持 1w QPS 时,可以设置 8000 来进行限流保护。线程数限流在客户端比较有效,例如在远程调用时我们设置连接池的线程数,超出这个并发线程请求,就将线程进行排队或者直接超时丢弃。</p>
<p>限流无疑会影响用户的正常请求,所以必然会导致一部分用户请求失败,因此在系统处理这种异常时一定要设置超时时间,防止因被限流的请求不能 fast fail(快速失败)而拖垮系统。</p>
<h3>4. 拒绝服务</h3>
<p>系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。</p>
<ul>
<li>
<h4>计过载保护</h4>
<pre><code>在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。</code></pre>
<p>拒绝服务是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。</p>
</li>
</ul>
<hr />
<p>♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥</p>
<h2>七、缓存失效策略</h2>
<p>2019-11-23 周六</p>
<p>有 Cache 的地方就必然存在失效问题(因为要保证数据的一致性)</p>
<h3>1. 被动失效</h3>
<p>主要处理如模板变更和一些对时效性不太敏感数据的失效,采用设置一定时间长度(如只缓存 3 秒钟)这种自动失效的方式。当然,你也要开发一个后台管理界面,以便能够在紧急情况下手工失效某些 Cache。</p>
<h3>2. 主动失效</h3>
<p>一般有 Cache 失效中心监控数据库表变化发送失效请求、系统发布也需要清空 Cache 数据等几种场景。其中失效中心承担了主要的失效功能,这个失效中心的逻辑图如下:
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/c093d8ba854a0dff22b513fd3b68487b?showdoc=.jpg" alt="" />
失效中心会监控关键数据表的变更(有个中间件来解析 MySQL 的 binglog,然后发现有 Insert、Update、Delete 等操作时,会把变更前的数据以及要变更的数据转成一个消息发送给订阅方),通过这种方式来发送失效请求给 Cache,从而清除 Cache 数据。如果 Cache 数据放在 CDN 上,那么也可以采用类似的方式来设计级联的失效结构,采用主动发请求给 Cache 软件失效的方式,如下图所示:
<img src="https://www.showdoc.cc/server/api/common/visitfile/sign/a27d74867d28787ce47f70c956ed3d4e?showdoc=.jpg" alt="" />
这种失效有失效中心将失效请求发送给每个 CDN 节点上的 Console 机,然后 Console 机来发送失效请求给每台 Cache 机器。</p>
<h3>3. 缓存命中率</h3>
<p>Cache 机器越多,命中率越低。
(Cache 实例越多,那么这些 Cache 缓存数据需要访问的次数也就越多。)</p>
<pre><code>例如有 3 个 Redis 实例,需要 3 个 Redis 实例都缓存商品 A,那么至少需要访问 3 次才行,而且是这 3 次访问刚好落到不同的 Redis 实例中。那么从第 4 次访问开始才会被命中,如果仅仅是一个 Redis 实例,那么第二次访问时其实就能命中了。所以理论上 Cache 实例多会影响命中率。</code></pre>
<p>如果访问量足够大,那么只是影响前几次命中率。</p>
<pre><code>如果 Cache 一直不失效的话是这样的,但是在实际的生产环境中 Cache 失效是很频繁发生的事情。很多情况下,还没等到所有 Cache 实例填满,该商品就已经失效了。因此要根据商品的重复访问量来合理地设置 Cache 分组。</code></pre>