<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>系统设计 on Ther 的博客</title>
        <link>https://blog.ther.cool/categories/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</link>
        <description>Recent content in 系统设计 on Ther 的博客</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sat, 16 Jul 2022 22:05:41 +0800</lastBuildDate><atom:link href="https://blog.ther.cool/categories/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>评论系统架构设计</title>
        <link>https://blog.ther.cool/posts/%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/</link>
        <pubDate>Sat, 16 Jul 2022 22:05:41 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/</guid>
        <description>&lt;h2 id=&#34;功能模块&#34;&gt;功能模块&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;发布评论: 支持回复楼层、楼中楼。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读取评论: 按照时间、热度排序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除评论: 用户删除、作者删除。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理评论: 作者置顶、后台运营管理(搜索、删除、审核等)。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;架构设计&#34;&gt;架构设计&lt;/h2&gt;
&lt;p&gt;架构设计等同于数据设计，梳理清楚数据的走向和逻辑。尽量&lt;strong&gt;避免环形依赖&lt;/strong&gt;、&lt;strong&gt;数据双向请求&lt;/strong&gt;等。&lt;/p&gt;
&lt;p&gt;概览图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/UZyRVMrTXdxPNHs.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716200753155&#34;
	
	
&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BFF: comment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;复杂评论业务的服务编排，比如访问账号服务进行等级判定，同时需要在 BFF 面向移动端/WEB场景来设计 API，这一层抽象把评论的本身的内容列表处理(加载、分页、排序等)进行了隔离，关注在业务平台化逻辑上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Service: comment-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务层，去平台业务的逻辑，专注在评论功能的 API 实现上，比如发布、读取、删除等，关注在稳定性、可用性上，这样让上游可以灵活组织逻辑把基础能力和业务能力剥离。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Job: comment-job&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消息队列的最大用途是削峰处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Admin: comment-admin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;管理平台，按照安全等级划分服务，尤其划分运营平台，它们会共享服务层的存储层(MySQL、Redis)。运营体系的数据大量都是检索，使用 Canal 将 MySQL 中的数据同步到 ES 中，整个数据的展示都是通过 ES，再通过业务主键更新业务数据层，这样运营端的查询压力就下方给了独立的 fulltext search 系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dependency: account-service、filter-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;评论服务还会依赖一些外部 gRPC 服务，统一的平台业务逻辑在 comment BFF 层收敛，这里 account-service 主要是账号服务，filter-service 是敏感词过滤服务。&lt;/p&gt;
&lt;h3 id=&#34;comment-service&#34;&gt;comment-service&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/eKAs2cDvXox1dbU.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716201722693&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment-service，专注在评论数据处理(认真想下 Separation of Concerns)。&lt;/p&gt;
&lt;p&gt;最开始 comment-service 和 comment 是一层，业务和功能耦合在一起，非常不利于迭代，当然在设计层面可以考虑目录结构进行拆分，但是架构层次来说，迭代隔离也是好的。&lt;/p&gt;
&lt;h4 id=&#34;读的核心逻辑&#34;&gt;读的核心逻辑&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Cache-Aside&lt;/strong&gt; 模式，先读取缓存，再读取存储。早期 cache rebuild 是做到服务里的，对于重建逻辑，一般会使用 read ahead 的思路，即预读，用户访问了第一页，很有可能访问第二页，所以缓存会超前加载，避免频繁 cache miss。但是当缓存抖动时，这种做法特别容易引起集群 thundering herd 现象，大量的请求会触发 cache rebuild，因为使用了预加载，容易导致加载大量数据到服务中，最终导致 OOM。&lt;/p&gt;
&lt;p&gt;所以回源的逻辑使用了消息队列来进行异步化。当缓存 miss 时，对于当前请求只返回 MySql 中部分数据即止，同时向 KafKa 中发送(回源)指令消息。&lt;/p&gt;
&lt;h4 id=&#34;写的核心逻辑&#34;&gt;写的核心逻辑&lt;/h4&gt;
&lt;p&gt;写和读相比较，写可以认为是透穿到存储层的，系统的瓶颈往往就来自于存储层，或者说是有状态层。对于写的设计上，可以认为刚发布的评论有极短的延迟(通常小于几 ms)对用户可见是可接受的，把对存储的直接冲击下放到消息队列，按照&lt;strong&gt;消息反压&lt;/strong&gt;的思路，即如果存储 latency 升高，消费能力就下降，自然消息容易堆积，系统始终以最大化方式消费。&lt;/p&gt;
&lt;p&gt;Kafka 存在 partition 概念，可以认为是物理上的一个小队列，一个 topic 由一组 partition 组成，所以 Kafka 的吞吐模型理解为：全局并行，局部串行的生产消费方式。对于入队的消息，可以按照 &lt;code&gt;hash(comment_subject) % N(partitions)&lt;/code&gt; 的方式进行分发。那么某个 partition 中的评论主题的数据一定都在一起，这样方便进行串行消费。在更新时，先更新 DB，再更新缓存。&lt;/p&gt;
&lt;p&gt;同样的，处理回源消息也是类似的思路。接收到消息后，可以先判断下缓存是否已经 rebuild，如果是则直接返回消息处理成功。&lt;/p&gt;
&lt;h3 id=&#34;comment-admin&#34;&gt;comment-admin&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/hQOptoklDLRbeB8.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716204038573&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;MySQL binlog 中的数据被 Canal 中间件流式消费，获取到业务的原始 CUD 操作(没有Retrive)，需要回放写入到 es 中，但是 es 中的数据最终是面向运营体系提供服务能力，需要检索的数据维度比较多，在写入 es 前需要做一个异构的 joiner，把单表变宽，预处理好 join 逻辑，然后写入到 es中。&lt;/p&gt;
&lt;p&gt;一般来说，运营后台的检索条件都是组合的，使用 es 的好处是避免依赖 mysql 来做多条件组合检索，同时 mysql 毕竟是 OLTP 面向线上联机事务处理的。通过冗余数据的方式，将多条件组合检索需求通过其他引擎来实现。&lt;/p&gt;
&lt;p&gt;此外 es 一般会存储检索、展示 primary key 等数据，当操作编辑的时候，找到记录的 primary key，交由 comment-admin 进行运营侧的 CRUD 操作。写操作对 DB 进行操作，经由 Canal 同步至 ES，而不是直接操作写 ES。&lt;/p&gt;
&lt;h3 id=&#34;comment&#34;&gt;comment&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/5VEWsuA2yh1qIDr.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716204817745&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment 作为 BFF，是面向端、平台、业务组合的服务。所以平台扩展的能力都在 comment 服务实现，方便以统一的接口形式提供平台化的能力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;依赖其他 gRPC 服务，整合统一平台测的逻辑(比如发布评论用户等级限定)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接向端上提供接口，提供数据的读写接口，甚至可以整合端上，提供统一的端上 SDK。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要在非核心依赖的 gRPC 服务不稳定时进行降级。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;存储设计&#34;&gt;存储设计&lt;/h2&gt;
&lt;h3 id=&#34;数据库设计&#34;&gt;数据库设计&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/u4PwJjbIzds2YLV.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716221909268&#34;
	
	
&gt;&lt;/p&gt;
&lt;h4 id=&#34;数据写入&#34;&gt;数据写入&lt;/h4&gt;
&lt;p&gt;事务更新 comment_subject，comment_index，comment_content 三张表，其中 content 属于非强制需要一致性考虑的。可以先写入 content，之后事务更新其他表。即便 content 先成功，后续失败，也仅存在一条 ghost 数据。&lt;/p&gt;
&lt;h4 id=&#34;数据读取&#34;&gt;数据读取&lt;/h4&gt;
&lt;p&gt;基于 obj_id + obj_type 在 comment_index 表找到评论列表，WHERE root = 0 ORDER BY floor。之后根据 comment_index 的 id 字段捞出 comment_content 的评论内容。对于二级的子楼层，WHERE parent/root IN (id&amp;hellip;)。&lt;/p&gt;
&lt;p&gt;因为产品形态上只存在二级列表，因此只需要迭代查询两次即可。对于嵌套层次多的，产品上，可以通过二次点击支持。&lt;/p&gt;
&lt;p&gt;Tips: 是不是可以使用 Graph 存储？DGraph、HugeGraph 类似的图存储思路。&lt;/p&gt;
&lt;h4 id=&#34;索引与内容分离&#34;&gt;索引与内容分离&lt;/h4&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/30/bkxO2q6iTB5Wnyc.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717182945045&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment_index: 评论楼层的索引组织表，实际并不包含内容。
comment_content: 评论内容的表，包含评论的具体内容。其中 comment_index 的 id 字段和 comment_content 是 1 对 1 的关系，这里面包含几种设计思想。&lt;/p&gt;
&lt;p&gt;表都有主键，即 cluster index，是物理组织形式存放的，comment_content 没有 id，是为了减少一次二级索引查找，直接基于主键检索，同时 comment_id 在写入要尽可能的顺序自增。&lt;/p&gt;
&lt;p&gt;索引、内容分离，方便 MySQL datapage 缓存更多的 row ，如果和 context 耦合，会导致更大的  IO。长远来看 content 信息可以直接使用 KV storage 存储。&lt;/p&gt;
&lt;h3 id=&#34;缓存设计&#34;&gt;缓存设计&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/17/7ArYIHQ21il6Gtg.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717225941315&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment_subject_cache: 对应主题的缓存，value 使用 protobuf 序列化的方式存入。早期使用 memcache 来进行缓存，因为 Redis 早期单线程模型，吞吐能力不高。&lt;/p&gt;
&lt;p&gt;comment_index_cache: 使用 Redis sortedset 进行索引的缓存，索引即数据的组织顺序，而非数据内容。参考过百度的贴吧，他们使用自己研发的拉链存储来组织索引，我认为 mysql 作为主力存储，利用 redis 来做加速完全足够，因为 cache miss 的构建，我们前面讲过使用 kafka 的消费者中处理，预加载少量数据，通过增量加载的方式逐渐预热填充缓存，而 redis sortedset skiplist 的实现，可以做到 &lt;code&gt;O(logN) + O(M)&lt;/code&gt; 的时间复杂度，效率很高。&lt;/p&gt;
&lt;p&gt;sorted set 是要增量追加的，因此必须判定 key 存在，才能 zdd。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么判定 key 存在后才能 add？&lt;/p&gt;
&lt;p&gt;若直接 add，则可能 key 已不存在，结果就是只 add 了要追加的一部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;comment_content_cache: 对应评论内容数据，使用 protobuf 序列化的方式存入。类似的我们早期使用 memcache 进行缓存。&lt;/p&gt;
&lt;p&gt;增量加载 + lazy 加载。&lt;/p&gt;
&lt;h2 id=&#34;可用性设计&#34;&gt;可用性设计&lt;/h2&gt;
&lt;h3 id=&#34;singleflight&#34;&gt;Singleflight&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/17/y4qRxKWImXkAvpg.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717230755891&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;对于热门的主题，如果存在缓存穿透的情况，会导致大量的同进程、跨进程的数据回源到存储层，可能会引起存储过载的情况，如何只交给同进程内，一个人去做加载存储？&lt;/p&gt;
&lt;p&gt;使用归并回源的思路:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://pkg.go.dev/golang.org/x/sync/singleflight&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;同进程只交给一个人去获取 mysql 数据，然后批量返回。同时这个 lease owner 投递一个 kafka 消息，做 index cache 的 recovery 操作。这样可以大大减少 mysql 的压力，以及大量透穿导致的密集写 kafka 的问题。&lt;/p&gt;
&lt;p&gt;更进一步的，后续连续的请求，仍然可能会短时 cache miss，我们可以在进程内设置一个 short-lived flag，标记最近有一个人投递了 cache rebuild 的消息，直接 drop。&lt;/p&gt;
&lt;p&gt;为什么我们不用分布式锁之类的思路？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分布式锁不好做，且太重。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;热点&#34;&gt;热点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/17/sHDIyce7glU1V5E.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717231201889&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;流量热点是因为突然热门的主题，被高频次的访问，因为底层的 cache 设计，一般是按照主题 key 进行一致性 hash 来进行分片，但是热点 key 一定命中某一个节点，这时候 remote cache 可能会变为瓶颈，因此做 cache 的升级 local cache 是有必要的，我们一般使用单进程自适应发现热点的思路，附加一个短时的 ttl local cache，可以在进程内吞掉大量的读请求。&lt;/p&gt;
&lt;p&gt;在内存中使用 hashmap 统计每个 key 的访问频次，这里可以使用滑动窗口统计，即每个窗口中，维护一个 hashmap，之后统计所有未过去的 bucket，汇总所有 key 的数据。之后使用小堆计算 TopK 的数据，自动进行热点识别。&lt;/p&gt;</description>
        </item>
        <item>
        <title>秒杀系统的设计</title>
        <link>https://blog.ther.cool/posts/%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1/</link>
        <pubDate>Sat, 21 Nov 2020 22:26:24 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1/</guid>
        <description>&lt;p&gt;秒杀系统本质上就是一个满足&lt;strong&gt;高并发&lt;/strong&gt;、&lt;strong&gt;高性能&lt;/strong&gt;和&lt;strong&gt;高可用&lt;/strong&gt;的&lt;strong&gt;分布式系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;秒杀其实主要解决两个问题，一个是&lt;strong&gt;并发读&lt;/strong&gt;，一个是&lt;strong&gt;并发写&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;并发读的核心优化理念是尽量减少用户到服务端来“读”数据，或者让他们读更少的数据；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并发写的处理原则也一样，它要求我们&lt;strong&gt;在数据库层面独立出来一个库&lt;/strong&gt;，做特殊的处理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，我们还要针对秒杀系统做一些保护，针对意料之外的情况设计兜底方案，以防止最坏的情况发生。&lt;/p&gt;
&lt;p&gt;从一个架构师的角度来看，要想打造并维护一个超大流量并发读写、高性能、高可用的系统，在整个用户请求路径上从浏览器到服务端我们要遵循几个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证用户请求的数据尽量少&lt;/li&gt;
&lt;li&gt;请求数尽量少&lt;/li&gt;
&lt;li&gt;路径尽量短&lt;/li&gt;
&lt;li&gt;依赖尽量少&lt;/li&gt;
&lt;li&gt;不要有单点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;秒杀的整体架构可以概括为“稳、准、快”几个关键字。&lt;/p&gt;
&lt;p&gt;“稳”，就是整个系统架构要&lt;strong&gt;满足高可用&lt;/strong&gt;，流量符合预期时肯定要稳定，就是超出预期时也同样不能掉链子，你要保证秒杀活动顺利完成，即秒杀商品顺利地卖出去，这个是最基本的前提。&lt;/p&gt;
&lt;p&gt;“准”，就是秒杀 10 台 iPhone，那就只能成交 10 台，多一台少一台都不行。一旦库存不对，那平台就要承担损失，所以“准”就是要求&lt;strong&gt;保证数据的一致性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;“快”，它就是说系统的&lt;strong&gt;性能要足够高&lt;/strong&gt;，否则你怎么支撑这么大的流量呢？不光是服务端要做极致的性能优化，而且在整个请求链路上都要做协同的优化，每个地方快一点，整个系统就完美了。&lt;/p&gt;
&lt;p&gt;所以从技术角度上看“稳、准、快”，就对应了我们架构上的&lt;strong&gt;高可用&lt;/strong&gt;、&lt;strong&gt;一致性&lt;/strong&gt;和&lt;strong&gt;高性能&lt;/strong&gt;的要求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高性能。&lt;/strong&gt; 秒杀涉及大量的&lt;strong&gt;并发读&lt;/strong&gt;和&lt;strong&gt;并发写&lt;/strong&gt;，因此支持高并发访问这点非常关键。将从设计&lt;strong&gt;数据的动静分离&lt;/strong&gt;方案、&lt;strong&gt;热点的发现与隔离&lt;/strong&gt;、&lt;strong&gt;请求的削峰与分层过滤&lt;/strong&gt;、&lt;strong&gt;服务端的极致优化&lt;/strong&gt;这 4 个方面优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性。&lt;/strong&gt; 秒杀中商品减库存的实现方式同样关键。可想而知，有限数量的商品在同一时刻被很多倍的请求同时来减库存，减库存又分为“&lt;strong&gt;拍下减库存&lt;/strong&gt;”“&lt;strong&gt;付款减库存&lt;/strong&gt;”以及&lt;strong&gt;预扣库存&lt;/strong&gt;等几种，在大并发更新的过程中都要保证数据的准确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可用。&lt;/strong&gt; 虽然有很多极致的优化思路，但现实中总难免出现一些考虑不到的情况，所以要保证系统的高可用和正确性，我们还要设计一个 PlanB 来&lt;strong&gt;兜底&lt;/strong&gt;，以便在最坏情况发生时仍然能够从容应对。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;1-架构原则4-要-1-不要&#34;&gt;1. 架构原则：“4 要 1 不要”&lt;/h2&gt;
&lt;h3 id=&#34;数据要尽量少&#34;&gt;数据要尽量少&lt;/h3&gt;
&lt;p&gt;所谓“数据要尽量少”，首先是指用户请求的数据能少就少。请求的数据包括&lt;strong&gt;上传给系统的数据&lt;/strong&gt;和&lt;strong&gt;系统返回给用户的数据&lt;/strong&gt;（通常就是网页）。&lt;/p&gt;
&lt;p&gt;为什么“数据要尽量少”呢？因为首先这些数据在网络上传输需要时间，其次不管是请求数据还是返回数据都需要服务器做处理，而服务器在写网络时通常都要做压缩和字符编码，这些都非常消耗 CPU，所以减少传输的数据量可以显著减少 CPU 的使用。例如，我们可以简化秒杀页面的大小，去掉不必要的页面装饰效果等等。&lt;/p&gt;
&lt;p&gt;其次，“数据要尽量少”还要求&lt;strong&gt;系统依赖的数据&lt;/strong&gt;能少就少，包括系统完成某些业务逻辑需要读取和保存的数据，这些数据一般是和后台服务以及数据库打交道的。&lt;strong&gt;调用其他服务会涉及数据的序列化和反序列化&lt;/strong&gt;，而这也是 CPU 的一大杀手，同样也会增加延时。而且，数据库本身也容易成为一个瓶颈，所以和数据库打交道越少越好，数据越简单、越小则越好。&lt;/p&gt;
&lt;h3 id=&#34;请求数要尽量少&#34;&gt;请求数要尽量少&lt;/h3&gt;
&lt;p&gt;用户请求的页面返回后，浏览器渲染这个页面还要包含其他的额外请求，比如说，这个页面依赖的 CSS / JavaScript、图片，以及 Ajax 请求等等都定义为“&lt;strong&gt;额外请求&lt;/strong&gt;”，这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗，例如建立连接要做三次握手，有的时候有页面依赖或者连接数限制，一些请求（例如 JavaScript）还需要串行加载等。另外，如果不同请求的域名不一样的话，还涉及这些&lt;strong&gt;域名的 DNS 解析&lt;/strong&gt;，可能会耗时更久。所以减少请求数可以显著减少以上这些因素导致的资源消耗。&lt;/p&gt;
&lt;p&gt;例如，&lt;strong&gt;减少请求数最常用的一个实践&lt;/strong&gt;就是&lt;strong&gt;合并 CSS 和 JavaScript 文件&lt;/strong&gt;，&lt;strong&gt;把多个 JavaScript 文件合并成一个文件&lt;/strong&gt;，&lt;strong&gt;在 URL 中用逗号隔开&lt;/strong&gt;，如：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这种方式&lt;strong&gt;在服务端仍然是单个文件各自存放&lt;/strong&gt;，只是&lt;strong&gt;服务端会有一个组件解析这个 URL&lt;/strong&gt;，然后动态把这些&lt;strong&gt;文件合并&lt;/strong&gt;起来一起返回。&lt;/p&gt;
&lt;h3 id=&#34;路径要尽量短&#34;&gt;路径要尽量短&lt;/h3&gt;
&lt;p&gt;所谓“路径”，就是用户发出请求到返回数据这个过程中，需求经过的中间的节点数。&lt;/p&gt;
&lt;p&gt;通常，这些节点可以表示为一个系统或者一个新的 Socket 连接（比如代理服务器只是创建一个新的 Socket 连接来转发请求）。每经过一个节点，一般都会产生一个新的 Socket 连接。&lt;/p&gt;
&lt;p&gt;然而，每增加一个连接都会增加新的不确定性。从概率统计上来说，假如一次请求经过 5 个节点，每个节点的可用性是 99.9% 的话，那么整个请求的可用性是：99.9% 的 5 次方，约等于 99.5%。&lt;/p&gt;
&lt;p&gt;所以&lt;strong&gt;缩短请求路径不仅可以增加可用性&lt;/strong&gt;，同样可以有效&lt;strong&gt;提升性能&lt;/strong&gt;（减少中间节点可以&lt;strong&gt;减少数据的序列化与反序列化&lt;/strong&gt;），并&lt;strong&gt;减少延时&lt;/strong&gt;（可以减少网络传输耗时）。&lt;/p&gt;
&lt;p&gt;要缩短访问路径有一种办法，就是&lt;strong&gt;多个相互强依赖的应用合并部署在一起&lt;/strong&gt;，把&lt;strong&gt;远程过程调用（RPC）变成 JVM 内部之间的方法调用&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;依赖要尽量少&#34;&gt;依赖要尽量少&lt;/h3&gt;
&lt;p&gt;所谓依赖，指的是要完成一次用户请求必须依赖的系统或者服务，这里的依赖指的是&lt;strong&gt;强依赖&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举个例子，比如说要展示秒杀页面，而这个页面必须强依赖商品信息、用户信息，还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息（弱依赖），这些弱依赖在紧急情况下就可以去掉。&lt;/p&gt;
&lt;p&gt;要减少依赖，我们可以给系统进行分级，比如 0 级系统、1 级系统、2 级系统、3 级系统，0 级系统如果是最重要的系统，那么 0 级系统强依赖的系统也同样是最重要的系统，以此类推。&lt;/p&gt;
&lt;p&gt;注意，0 级系统要尽量减少对 1 级系统的强依赖，防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统，而优惠券是 1 级系统的话，在极端情况下可以把优惠券给降级，防止支付系统被优惠券这个 1 级系统给拖垮。&lt;/p&gt;
&lt;h3 id=&#34;不要有单点&#34;&gt;不要有单点&lt;/h3&gt;
&lt;p&gt;系统中的单点可以说是系统架构上的一个大忌，因为单点意味着没有备份，风险不可控，设计分布式系统最重要的原则就是“消除单点”。&lt;/p&gt;
&lt;p&gt;那如何避免单点呢？关键是&lt;strong&gt;避免将服务的状态和机器绑定&lt;/strong&gt;，即把&lt;strong&gt;服务无状态化&lt;/strong&gt;，这样服务就可以在机器中随意移动。&lt;/p&gt;
&lt;p&gt;如何那把服务的状态和机器解耦呢？这里也有很多实现方式。例如把&lt;strong&gt;和机器相关的配置动态化&lt;/strong&gt;，这些参数可以通过&lt;strong&gt;配置中心&lt;/strong&gt;来&lt;strong&gt;动态推送&lt;/strong&gt;，在服务启动时动态拉取下来，在这些配置中心设置一些规则来方便地改变这些映射关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;应用无状态化是有效避免单点的一种方式&lt;/strong&gt;，但是像&lt;strong&gt;存储服务&lt;/strong&gt;本身很难无状态化，因为数据要存储在磁盘上，本身就要和机器绑定，那么这种场景一般要通过&lt;strong&gt;冗余多个备份&lt;/strong&gt;的方式来解决单点问题。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;那是不是请求最少就一定最好？不一定。把 CSS 内联进页面里，这样做可以减少依赖一个 CSS 的请求从而加快首页的渲染，但是同样也增大了页面的大小，又不符合“数据要尽量少”的原则，这种情况下我们为了提升首屏的渲染速度，只把首屏的 HTML 依赖的 CSS 内联进来，其他 CSS 仍然放到文件中作为依赖加载，尽量实现首屏的打开速度与整个页面加载性能的平衡。&lt;/p&gt;
&lt;p&gt;所以说，&lt;strong&gt;架构是一种平衡的艺术，而最好的架构一旦脱离了它所适应的场景，一切都将是空谈&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;不同场景下的不同架构案例&#34;&gt;不同场景下的不同架构案例&lt;/h3&gt;
&lt;p&gt;以淘宝早期秒杀系统架构的演进为主线，来梳理不同的请求体量下的最佳秒杀系统架构。&lt;/p&gt;
&lt;p&gt;如果你想快速搭建一个简单的秒杀系统，只需要把你的商品购买页面增加一个“定时上架”功能，仅在秒杀开始时才让用户看到购买按钮，当商品的库存卖完了也就结束了。这就是当时第一个版本的秒杀系统实现方式。&lt;/p&gt;
&lt;p&gt;但随着请求量的加大（比如从 1w/s 到了 10w/s 的量级），这个简单的架构很快就遇到了瓶颈，因此需要做架构改造来提升系统性能。这些架构改造包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把秒杀系统独立出来&lt;strong&gt;单独打造一个系统&lt;/strong&gt;，这样可以有针对性地做优化，例如这个独立出来的系统就减少了店铺装修的功能，减少了页面的复杂度；&lt;/li&gt;
&lt;li&gt;在系统部署上也&lt;strong&gt;独立做一个机器集群&lt;/strong&gt;，这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载；&lt;/li&gt;
&lt;li&gt;将&lt;strong&gt;热点数据&lt;/strong&gt;（如库存数据）&lt;strong&gt;单独放到一个缓存系统&lt;/strong&gt;中，以提高“读性能”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增加秒杀答题&lt;/strong&gt;，防止有秒杀器抢单。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时的系统架构变成了下图这个样子。最重要的就是，秒杀详情成为了一个独立的新系统，另外核心的一些数据放到了缓存（Cache）中，其他的关联系统也都以独立集群的方式进行部署。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/2pUyQZhVG8B4YFH.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;然而这个架构仍然支持不了超过 100w/s 的请求量，所以为了进一步提升秒杀系统的性能，又对架构做进一步升级，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;strong&gt;页面&lt;/strong&gt;进行彻底的&lt;strong&gt;动静分离&lt;/strong&gt;，使得用户秒杀时不需要刷新整个页面，而只需要点击抢宝按钮，借此把页面刷新的数据降到最少；&lt;/li&gt;
&lt;li&gt;在服务端对秒杀商品进行&lt;strong&gt;本地缓存&lt;/strong&gt;，&lt;strong&gt;不需要再调用依赖系统的后台服务&lt;/strong&gt;获取数据，甚至&lt;strong&gt;不需要去公共的缓存集群中查询&lt;/strong&gt;数据，这样不仅可以减少系统调用，而且能够避免压垮公共缓存集群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增加系统限流保护&lt;/strong&gt;，防止最坏情况发生。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注：数据缓存在机器内存中的话，集群内如何实现多台机器数据一致性？&lt;/p&gt;
&lt;p&gt;答：&lt;/p&gt;
&lt;p&gt;在内存的数据是&lt;strong&gt;静态数据&lt;/strong&gt;，不会更新，没有一致性问题。&lt;/p&gt;
&lt;p&gt;库存不会放在本地缓存，本地缓存只放静态数据。库存是放在独立的缓存系统里，如 Redis，库存是采用&lt;strong&gt;主动失效&lt;/strong&gt;的方式来失效缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;被动失效：数据被访问时如果发现已经失效就删除。&lt;/p&gt;
&lt;p&gt;主动失效：周期性地从设置了失效时间的数据中选择一部分失效的数据删除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;经过这些优化，系统架构变成了下图中的样子。在这里，对页面进行了进一步的&lt;strong&gt;静态化&lt;/strong&gt;，秒杀过程中不需要刷新整个页面，而只需要向服务端请求很少的动态数据。而且，最关键的详情和交易系统都增加了&lt;strong&gt;本地缓存&lt;/strong&gt;，来提前缓存秒杀商品的信息，&lt;strong&gt;热点数据库也做了独立部署&lt;/strong&gt;，等等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/URivWHnoEXJfL7e.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;从前面的几次升级来看，其实越到后面需要定制的地方越多，也就是越“&lt;strong&gt;不通用&lt;/strong&gt;”。例如，把秒杀商品缓存在每台机器的内存中，这种方式显然不适合太多的商品同时进行秒杀的情况，因为单机的内存始终有限。&lt;strong&gt;所以要取得极致的性能，就要在其他地方（比如，通用性、易用性、成本等方面）有所牺牲&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;2-动静分离&#34;&gt;2. 动静分离&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;秒杀的场景中，对于系统的要求其实就三个字：快、准、稳&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那怎么才能“&lt;strong&gt;快&lt;/strong&gt;”起来呢？抽象起来讲就只有两点，一点是&lt;strong&gt;提高单次请求的效率&lt;/strong&gt;，一点是&lt;strong&gt;减少没必要的请求&lt;/strong&gt;。“动静分离”就是瞄着这个大方向去的。&lt;/p&gt;
&lt;p&gt;最早的秒杀系统其实是要刷新整体页面的，但后来秒杀的时候，只要点击“刷新抢宝”按钮就够了，这种变化的本质就是动静分离，分离之后，客户端大幅度减少了请求的数据量。这不自然就“快”了吗？&lt;/p&gt;
&lt;h3 id=&#34;何为动静数据&#34;&gt;何为动静数据&lt;/h3&gt;
&lt;p&gt;那到底什么才是动静分离呢？所谓“动静分离”，其实就是&lt;strong&gt;把用户请求的数据（如 HTML 页面）划分为“动态数据”和“静态数据”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简单来说，&lt;strong&gt;“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关，以及是否含有 Cookie 等私密数据&lt;/strong&gt;。比如说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;很多媒体类的网站，某一篇文章的内容不管是你访问还是我访问，它都是一样的。所以它就是一个&lt;strong&gt;典型的静态数据&lt;/strong&gt;，&lt;strong&gt;但是它是个动态页面&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果现在访问淘宝的首页，每个人看到的页面可能都是不一样的，淘宝首页中包含了很多根据访问者特征推荐的信息，而这些个性化的数据就可以理解为动态数据了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里再强调一下，我们所说的&lt;strong&gt;静态数据&lt;/strong&gt;，不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面，它&lt;strong&gt;也可能是经过 Java 系统产生的页面&lt;/strong&gt;，但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”，并&lt;strong&gt;不是说数据本身是否动静&lt;/strong&gt;，而是数据中&lt;strong&gt;是否含有和访问者相关的个性化数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;还有一点要注意，就是页面中“不包含”，指的是“页面的 HTML 源码中不含有”，这一点务必要清楚。&lt;/p&gt;
&lt;p&gt;分离了动静数据，就可以&lt;strong&gt;对分离出来的静态数据做缓存&lt;/strong&gt;，有了缓存之后，静态数据的“访问效率”自然就提高了。&lt;/p&gt;
&lt;p&gt;那么怎样对静态数据做缓存呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，应该把静态数据缓存到离用户最近的地方&lt;/strong&gt;。静态数据就是那些相对不会变化的数据，因此我们可以把它们缓存起来。缓存到哪里呢？常见的就三种：用户&lt;strong&gt;浏览器&lt;/strong&gt;里、&lt;strong&gt;CDN&lt;/strong&gt; 上或者在&lt;strong&gt;服务端的 Cache&lt;/strong&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，静态化改造就是要直接缓存 HTTP 连接&lt;/strong&gt;。相较于普通的数据缓存而言，你肯定还听过系统的静态化改造。静态化改造是&lt;strong&gt;直接缓存 HTTP 连接&lt;/strong&gt;而不是仅仅缓存数据，如下图所示，Web 代理服务器根据请求 URL，直接取出对应的 HTTP 响应头和响应体然后直接返回，这个响应过程简单得连 HTTP 协议都不用重新组装，甚至连 HTTP 请求头也不需要解析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/rZUGlije38XvSMV.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;第三，&lt;strong&gt;让谁来缓存静态数据&lt;/strong&gt;也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例，因为 Java 系统本身也有其弱点（比如不擅长处理大量连接请求，每个连接消耗的内存较多，Servlet 容器解析 HTTP 协议较慢），所以可以不在 Java 层做缓存，而是直接在 &lt;strong&gt;Web 服务器层&lt;/strong&gt;上做，这样你就可以屏蔽 Java 语言层面的一些弱点；而相比起来，Web 服务器（如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。&lt;/p&gt;
&lt;h3 id=&#34;如何做动静分离的改造&#34;&gt;如何做动静分离的改造&lt;/h3&gt;
&lt;p&gt;如何把动态页面改造成适合缓存的静态页面呢？其实也很简单，就是去除前面所说的那几个影响因素，把它们单独分离出来，做动静分离。&lt;/p&gt;
&lt;p&gt;下面，以典型的商品详情系统为例来详细介绍。这里，可以先打开京东或者淘宝的商品详情页，看看这个页面里都有哪些动静数据。从以下 5 个方面来分离出动态内容。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;URL 唯一化&lt;/strong&gt;。商品详情系统天然地就可以做到 URL 唯一化，比如每个商品都由 ID 来标识，那么 &lt;a class=&#34;link&#34; href=&#34;http://item.xxx.com/item.htm?id=xxxx&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;http://item.xxx.com/item.htm?id=xxxx&lt;/a&gt; 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢？前面说了我们是&lt;strong&gt;要缓存整个 HTTP 连接&lt;/strong&gt;，那么以什么作为 Key 呢？就&lt;strong&gt;以 URL 作为缓存的 Key&lt;/strong&gt;，例如以 id=xxx 这个格式进行区分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分离浏览者相关的因素&lt;/strong&gt;。浏览者相关的因素包括是否已登录，以及登录身份等，这些相关因素可以单独拆分出来，通过动态请求来获取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分离时间因素&lt;/strong&gt;。服务端输出的时间也通过动态请求获取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步化地域因素&lt;/strong&gt;。详情页面上与地域相关的因素做成异步方式获取，当然也可以通过动态请求方式获取，只是这里通过异步获取更合适。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去掉 Cookie&lt;/strong&gt;。服务端输出的页面包含的 Cookie 可以通过代码软件来删除，如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意，这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了，而是说，&lt;strong&gt;在缓存的静态数据中不含有 Cookie&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分离出动态内容之后，如何组织这些内容页就变得非常关键了。注意：因为这其中很多动态内容都会被页面中的其他模块用到，如判断该用户是否已登录、用户 ID 是否匹配等，所以这个时候应该将这些&lt;strong&gt;信息 JSON 化&lt;/strong&gt;（用 JSON 格式组织这些数据），以方便前端获取。&lt;/p&gt;
&lt;p&gt;前面介绍里用缓存的方式来处理静态数据。而动态内容的处理通常有两种方案：ESI（Edge Side Includes）方案和 CSI（Client Side Include）方案。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ESI 方案（或者 SSI）&lt;/strong&gt;：即在 &lt;strong&gt;Web 代理服务器&lt;/strong&gt;上做动态内容请求，并将请求插入到静态页面中，当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响，但是用户体验较好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSI 方案&lt;/strong&gt;。即单独发起一个异步 JavaScript 请求，以向服务端获取动态内容。这种方式服务端性能更佳，但是用户端页面可能会延时，体验稍差。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;动静分离的几种架构方案&#34;&gt;动静分离的几种架构方案&lt;/h3&gt;
&lt;p&gt;前面通过改造把静态数据和动态数据做了分离，那么如何在系统架构上进一步对这些动态和静态数据重新组合，再完整地输出给用户呢？&lt;/p&gt;
&lt;p&gt;这就涉及对用户请求路径进行合理的架构了。根据架构上的复杂度，有 3 种方案可选：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实体机单机部署；&lt;/li&gt;
&lt;li&gt;统一 Cache 层；&lt;/li&gt;
&lt;li&gt;上 CDN。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&#34;方案-1实体机单机部署&#34;&gt;方案 1：实体机单机部署&lt;/h4&gt;
&lt;p&gt;这种方案是&lt;strong&gt;将虚拟机改为实体机&lt;/strong&gt;，以增大 Cache 的容量，并且采用了&lt;strong&gt;一致性 Hash 分组&lt;/strong&gt;的方式来提升命中率。这里将 Cache 分成若干组，是希望能达到命中率和访问热点的平衡。Hash 分组越少，缓存的命中率肯定就会越高，但短板是也会使单个商品集中在一个分组中，容易导致 Cache 被击穿，所以我们应该适当增加多个相同的分组，来&lt;strong&gt;平衡访问热点和命中率&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;p&gt;实体机单机部署方案的结构图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/KHhAgkmZQiFcTUt.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：LVS（Linux Virtual Server），Linux 虚拟服务器。在Linux内核中实现了基于 IP 的数据请求&lt;strong&gt;负载均衡&lt;/strong&gt;调度方案。&lt;/p&gt;
&lt;p&gt;实体机单机部署有以下几个优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有网络瓶颈，而且能使用大内存；&lt;/li&gt;
&lt;li&gt;既能提升命中率，又能减少 Gzip 压缩；&lt;/li&gt;
&lt;li&gt;减少 Cache 失效压力，因为采用定时失效方式，例如只缓存 3 秒钟，过期即自动失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个方案中，虽然把通常只需要虚拟机或者容器运行的 Java 应用换成实体机，优势很明显，它会增加单机的内存容量，但是一定程度上也造成了 CPU 的浪费，因为单个的 Java 进程很难用完整个实体机的 CPU。&lt;/p&gt;
&lt;p&gt;另外就是，一个实体机上部署了 Java 应用又作为 Cache 来使用，这造成了运维上的高复杂度，所以这是一个折中的方案。如果没有更多的系统有类似需求，那么这样做也比较合适，如果有多个业务系统都有静态化改造的需求，那还是建议把 Cache 层单独抽出来公用比较合理，如下面的方案 2 。&lt;/p&gt;
&lt;h4 id=&#34;方案-2统一-cache-层&#34;&gt;方案 2：统一 Cache 层&lt;/h4&gt;
&lt;p&gt;所谓统一 Cache 层，就是将单机的 Cache 统一分离出来，形成一个单独的 Cache 集群。统一 Cache 层是个更理想的可推广方案，该方案的结构图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/jXUiGbA538CDrlu.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;将 Cache 层单独拿出来统一管理可以&lt;strong&gt;减少运维成本&lt;/strong&gt;，同时也&lt;strong&gt;方便接入其他静态化系统&lt;/strong&gt;。此外，它还有一些优点。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单独一个 Cache 层，可以减少多个应用接入时使用 Cache 的成本。这样接入的应用只要维护自己的 Java 系统就好，不需要单独维护 Cache，而只关心如何使用即可。&lt;/li&gt;
&lt;li&gt;统一 Cache 的方案更易于维护，如后面加强监控、配置的自动化，只需要一套解决方案就行，统一起来维护升级也比较方便。&lt;/li&gt;
&lt;li&gt;可以共享内存，最大化利用内存，不同系统之间的内存可以动态切换，从而能够有效应对各种攻击。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种方案虽然维护上更方便了，但是也带来了其他一些问题，比如缓存更加集中，导致：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cache 层内部交换网络成为瓶颈；&lt;/li&gt;
&lt;li&gt;缓存服务器的网卡也会是瓶颈；&lt;/li&gt;
&lt;li&gt;机器少风险较大，挂掉一台就会影响很大一部分缓存数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要解决上面这些问题，&lt;strong&gt;可以再对 Cache 做 Hash 分组&lt;/strong&gt;，即一组 Cache 缓存的内容相同，这样能够避免热点数据过度集中导致新的瓶颈产生。&lt;/p&gt;
&lt;h4 id=&#34;方案-3上-cdn&#34;&gt;方案 3：上 CDN&lt;/h4&gt;
&lt;p&gt;在将整个系统做动静分离后，我们自然会想到更进一步的方案，就是将 Cache 进一步前移到 CDN 上，因为 CDN 离用户最近，效果会更好。&lt;/p&gt;
&lt;p&gt;但是要想这么做，有以下几个问题需要解决。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;失效问题&lt;/strong&gt;。前面也有提到过缓存时效的问题。谈到静态数据时，一个关键词叫“相对不变”，它的言外之意是“可能会变化”。比如一篇文章，现在不变，但如果发现个错别字，是不是就会变化了？如果缓存时效很长，那用户端在很长一段时间内看到的都是错的。所以，这个方案中也是，我们需要保证 CDN 可以在秒级时间内，让分布在全国各地的 Cache 同时失效，这对 CDN 的失效系统要求很高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命中率问题&lt;/strong&gt;。Cache 最重要的一个衡量指标就是“高命中率”，不然 Cache 的存在就失去了意义。同样，如果将数据全部放到全国的 CDN 上，必然导致 Cache 分散，而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低，那么命中率就成为一个问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布更新问题&lt;/strong&gt;。如果一个业务系统每周都有日常业务需要发布，那么发布系统必须足够简洁高效，而且你还要考虑有问题时快速回滚和排查问题的简便性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从前面的分析来看，将商品详情系统放到全国的所有 CDN 节点上是不太现实的，因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢？答案是“可以”，但是这样的节点需要满足几个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;靠近访问量比较集中的地区；&lt;/li&gt;
&lt;li&gt;离主站相对较远；&lt;/li&gt;
&lt;li&gt;节点到主站间的网络比较好，而且稳定；&lt;/li&gt;
&lt;li&gt;节点容量比较大，不会占用其他 CDN 太多的资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后，还有一点也很重要，那就是：节点不要太多。&lt;/p&gt;
&lt;p&gt;基于上面几个因素，选择 &lt;strong&gt;CDN 的二级 Cache&lt;/strong&gt; 比较合适，因为二级 Cache 数量偏少，容量也更大，让用户的请求先&lt;strong&gt;回源&lt;/strong&gt;的 CDN 的二级 Cache 中，如果没命中再回源站获取数据，部署方式如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/qdC5rTNyPnYHw8I.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;使用 CDN 的二级 Cache 作为缓存，可以达到和当前服务端静态化 Cache 类似的命中率，因为节点数不多，Cache 不是很分散，访问量也比较集中，这样也就解决了命中率问题，同时能够给用户最好的访问体验，是当前比较理想的一种 CDN 化方案。&lt;/p&gt;
&lt;p&gt;除此之外，CDN 化部署方案还有以下几个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把整个页面缓存在用户浏览器中；&lt;/li&gt;
&lt;li&gt;如果强制刷新整个页面，也会请求 CDN；&lt;/li&gt;
&lt;li&gt;实际有效请求，只是用户对“刷新抢宝”按钮的点击。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样就把 90% 的静态数据缓存在了用户端或者 CDN 上，当真正秒杀时，用户只需要点击特殊的“刷新抢宝”按钮，而不需要刷新整个页面。这样一来，系统只是向服务端请求很少的有效数据，而不需要重复请求大量的静态数据。&lt;/p&gt;
&lt;p&gt;秒杀的动态数据和普通详情页面的动态数据相比更少，性能也提升了 3 倍以上。所以“抢宝”这种设计思路，不用刷新页面就能够很好地请求到服务端最新的动态数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.存储在浏览器或 CDN 上，有多大区别？&lt;/p&gt;
&lt;p&gt;区别很大。因为在 CDN 上，可以做&lt;strong&gt;主动失效&lt;/strong&gt;，而在用户的浏览器里就更不可控，如果用户不主动刷新的话，很难主动地把消息推送给用户的浏览器。&lt;/p&gt;
&lt;p&gt;2.在什么地方把静态数据和动态数据合并并渲染出一个完整的页面？&lt;/p&gt;
&lt;p&gt;假如在用户的浏览器里合并，那么服务端可以减少渲染整个页面的 CPU 消耗。如果在服务端合并的话，就要考虑缓存的数据是否进行 Gzip 压缩了：如果缓存 Gzip 压缩后的静态数据可以减少缓存的数据量，但是进行页面合并渲染时就要先解压，然后再压缩完整的页面数据输出给用户；如果缓存未压缩的静态数据，这样不用解压静态数据，但是会增加缓存容量。虽然这些都是细节问题，但你在设计架构方案时都需要考虑清楚。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;3-处理系统热点数据&#34;&gt;3. 处理系统热点数据&lt;/h2&gt;
&lt;p&gt;假设系统中存储有几十亿上百亿的商品，而每天有千万级的商品被上亿的用户访问，那么肯定有一部分被大量用户访问的热卖商品，这就是我们常说的“热点商品”。&lt;/p&gt;
&lt;p&gt;这些热点商品中最极端的例子就是秒杀商品，它们在很短时间内被大量用户执行访问、添加购物车、下单等操作，这些操作就称为“热点操作”。那么问题来了：这些热点对系统有啥影响，我们为什么要关注这些热点？&lt;/p&gt;
&lt;h3 id=&#34;为什么要关注热点&#34;&gt;为什么要关注热点&lt;/h3&gt;
&lt;p&gt;我们一定要关注热点，因为热点会对系统产生一系列的影响。&lt;/p&gt;
&lt;p&gt;首先，热点请求会&lt;strong&gt;大量占用服务器处理资源&lt;/strong&gt;，虽然这个热点可能只占请求总量的亿分之一，然而却可能抢占 90% 的服务器资源，如果这个热点请求还是没有价值的无效请求，那么对系统资源来说完全是浪费。&lt;/p&gt;
&lt;p&gt;其次，即使这些热点是有效的请求，我们也要识别出来做针对性的优化，从而用更低的代价来支撑这些热点请求。&lt;/p&gt;
&lt;h3 id=&#34;什么是热点&#34;&gt;什么是“热点”&lt;/h3&gt;
&lt;p&gt;热点分为&lt;strong&gt;热点操作&lt;/strong&gt;和&lt;strong&gt;热点数据&lt;/strong&gt;。所谓“热点操作”，例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单等都属于此类操作。对系统来说，这些操作可以抽象为“&lt;strong&gt;读请求&lt;/strong&gt;”和“&lt;strong&gt;写请求&lt;/strong&gt;”，这两种热点请求的处理方式大相径庭，读请求的优化空间要大一些，而&lt;strong&gt;写请求的瓶颈一般都在存储层&lt;/strong&gt;，&lt;strong&gt;优化的思路就是根据 CAP 理论做平衡&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而“热点数据”比较好理解，那就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”。&lt;/p&gt;
&lt;p&gt;所谓“静态热点数据”，就是能够提前预测的热点数据。例如，可以通过卖家报名的方式提前筛选出来，通过报名系统对这些热点商品进行打标。另外，还可以通过大数据分析来提前发现热点商品，比如分析历史成交记录、用户的购物车记录，来发现哪些商品可能更热门、更好卖，这些都是可以提前分析出来的热点。&lt;/p&gt;
&lt;p&gt;所谓“动态热点数据”，就是不能被提前预测到的，系统在运行过程中临时产生的热点。例如，卖家在抖音上做了广告，然后商品一下就火了，导致它在短时间内被大量购买。&lt;/p&gt;
&lt;p&gt;由于热点操作是用户的行为，不好改变，但能做一些限制和保护，所以&lt;strong&gt;主要针对热点数据&lt;/strong&gt;来介绍如何进行&lt;strong&gt;优化&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;发现热点数据&#34;&gt;发现热点数据&lt;/h3&gt;
&lt;p&gt;前面介绍了如何对单个秒杀商品的页面数据进行动静分离，以便针对性地对静态数据做优化处理，那么另外一个关键的问题来了：如何发现这些秒杀商品，或者更准确地说，如何发现热点商品呢？&lt;/p&gt;
&lt;p&gt;你可能会说“参加秒杀的商品就是秒杀商品啊”，没错，关键是系统怎么知道哪些商品参加了秒杀活动呢？所以，你要有一个机制提前来区分普通商品和秒杀商品。&lt;/p&gt;
&lt;p&gt;从发现静态热点和发现动态热点两个方面来看一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发现静态热点数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如前面讲的，静态热点数据可以通过商业手段，例如强制让卖家通过报名参加的方式提前把热点商品筛选出来，实现方式是通过一个运营系统，把参加活动的商品数据进行打标，然后通过一个后台系统对这些热点商品进行预处理，如提前进行缓存。但是这种通过报名提前筛选的方式也会带来新的问题，即增加卖家的使用成本，而且实时性较差，也不太灵活。&lt;/p&gt;
&lt;p&gt;不过，除了提前报名筛选这种方式，你还可以通过技术手段提前预测，例如对买家每天访问的商品进行大数据计算，然后统计出 TOP N 的商品，可以认为这些 TOP N 的商品就是热点商品。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发现动态热点数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以通过卖家报名或者大数据预测这些手段来提前预测静态热点数据，但这其中有一个痛点，就是实时性较差，如果我们的系统能在秒级内自动发现热点商品那就完美了。&lt;/p&gt;
&lt;p&gt;能够动态地实时发现热点不仅对秒杀商品，对其他热卖商品也同样有价值，所以需要想办法实现热点的动态发现功能。&lt;/p&gt;
&lt;p&gt;这里给出一个动态热点发现系统的具体实现。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建一个异步的系统，它可以收集交易链路上各个环节中的中间件产品的热点 Key，如 Nginx、缓存、RPC 服务框架等这些中间件（一些中间件产品本身已经有热点统计模块）。&lt;/li&gt;
&lt;li&gt;建立一个热点上报和可以按照需求订阅的热点服务的下发规范，主要目的是通过交易链路上各个系统（包括详情、购物车、交易、优惠、库存、物流等）访问的时间差，把上游已经发现的热点透传给下游系统，提前做好保护。比如，对于大促高峰期，详情系统是最早知道的，在统一接入层上 Nginx 模块统计的热点 URL。&lt;/li&gt;
&lt;li&gt;将上游系统收集的热点数据发送到热点服务台，然后下游系统（如交易系统）就会知道哪些商品会被频繁调用，然后做热点保护。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里给出了一个图，其中用户访问商品时经过的路径有很多，主要是依赖前面的导购页面（包括首页、搜索页面、商品详情、购物车等）提前识别哪些商品的访问量高，通过这些系统中的中间件来收集热点数据，并记录到日志中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/SvY7UzjJwq1ycE6.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;通过部署在每台机器上的 Agent 把日志汇总到聚合和分析集群中，然后把符合一定规则的热点数据，通过订阅分发系统再推送到相应的系统中。可以把热点数据填充到 Cache 中，或者直接推送到应用服务器的内存中，还可以对这些数据进行拦截，总之下游系统可以订阅这些数据，然后根据自己的需求决定如何处理这些数据。&lt;/p&gt;
&lt;p&gt;打造热点发现系统时，注意以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个热点服务后台抓取热点数据日志最好采用&lt;strong&gt;异步&lt;/strong&gt;方式，因为“异步”一方面便于保证通用性，另一方面又不影响业务系统和中间件产品的主流程。&lt;/li&gt;
&lt;li&gt;热点服务发现和中间件自身的热点保护模块并存，每个中间件和应用还需要保护自己。热点服务台提供热点数据的收集和订阅服务，便于把各个系统的热点数据透明出来。&lt;/li&gt;
&lt;li&gt;热点发现要做到&lt;strong&gt;接近实时&lt;/strong&gt;（3s 内完成热点数据的发现），因为只有做到接近实时，动态发现才有意义，才能实时地对下游系统提供保护。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;处理热点数据&#34;&gt;处理热点数据&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;处理热点数据通常有几种思路：一是优化，二是限制，三是隔离&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;优化：优化热点数据最有效的办法就是缓存热点数据，如果热点数据做了动静分离，那么可以长期缓存静态数据。但是，缓存热点数据更多的是“临时”缓存，即不管是静态数据还是动态数据，都用一个队列短暂地缓存数秒钟，由于队列长度有限，可以采用 &lt;strong&gt;LRU 淘汰算法&lt;/strong&gt;替换。&lt;/p&gt;
&lt;p&gt;限制：限制更多的是一种保护机制，限制的办法也有很多，例如&lt;strong&gt;对被访问商品的 ID 做一致性 Hash&lt;/strong&gt;，&lt;strong&gt;然后根据 Hash 做分桶&lt;/strong&gt;，&lt;strong&gt;每个分桶设置一个处理队列&lt;/strong&gt;，这样可以把热点商品限制在一个请求队列里，防止因某些热点商品占用太多的服务器资源，而使其他请求始终得不到服务器的处理资源。&lt;/p&gt;
&lt;p&gt;隔离：秒杀系统设计的第一个原则就是将这种热点数据隔离出来，不要让 1% 的请求影响到另外的 99%，隔离出来后也更方便对这 1% 的请求做针对性的优化。&lt;/p&gt;
&lt;p&gt;具体到“秒杀”业务，我们可以在以下几个层次实现隔离。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;业务隔离&lt;/strong&gt;。把秒杀做成一种营销活动，卖家要参加秒杀这种营销活动需要单独报名，从技术上来说，卖家报名后对我们来说就有了已知热点，因此可以提前做好预热。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统隔离&lt;/strong&gt;。系统隔离更多的是运行时的隔离，可以通过&lt;strong&gt;分组部署&lt;/strong&gt;的方式和另外 99% 分开。秒杀可以申请单独的域名，目的也是&lt;strong&gt;让请求落到不同的集群&lt;/strong&gt;中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据隔离&lt;/strong&gt;。秒杀所调用的数据大部分都是热点数据，比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据，目的也是不想 0.01% 的数据有机会影响 99.99% 数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然了，实现隔离有很多种办法。比如，可以按照用户来区分，给不同的用户分配不同的 Cookie，在接入层，路由到不同的服务接口中；再比如，还可以在接入层针对 URL 中的不同 Path 来设置限流策略。服务层调用不同的服务接口，以及数据层通过给数据打标来区分等等这些措施，其目的都是把已经识别出来的热点请求和普通的请求区分开。&lt;/p&gt;
&lt;h2 id=&#34;4-流量削峰&#34;&gt;4. 流量削峰&lt;/h2&gt;
&lt;p&gt;如果看过秒杀系统的流量监控图的话，你会发现它是一条直线，就在秒杀开始那一秒是一条很直很直的线，这是因为秒杀请求在时间上高度集中于某一特定的时间点。这样一来，就会导致一个特别高的流量峰值，它对资源的消耗是瞬时的。&lt;/p&gt;
&lt;p&gt;但是对秒杀这个场景来说，最终能够抢到商品的人数是固定的，也就是说 100 人和 10000 人发起请求的结果都是一样的，并发度越高，无效请求也越多。&lt;/p&gt;
&lt;p&gt;但是从业务上来说，秒杀活动是希望更多的人来参与的，也就是开始之前希望有更多的人来刷页面，但是真正开始下单时，秒杀请求并不是越多越好。因此可以设计一些规则，让并发的请求更多地延缓，而且甚至可以过滤掉一些无效请求。&lt;/p&gt;
&lt;h3 id=&#34;为什么要削峰&#34;&gt;为什么要削峰&lt;/h3&gt;
&lt;p&gt;为什么要削峰呢？或者说峰值会带来哪些坏处？&lt;/p&gt;
&lt;p&gt;服务器的处理资源是恒定的，用或者不用它的处理能力都是一样的，所以出现峰值的话，很容易导致忙到处理不过来，闲的时候却又没有什么要处理。但是由于要保证服务质量，很多处理资源只能按照忙的时候来预估，而这会导致资源的一个浪费。&lt;/p&gt;
&lt;p&gt;这就好比因为存在早高峰和晚高峰的问题，所以有了错峰限行的解决方案。削峰的存在，一是可以&lt;strong&gt;让服务端处理变得更加平稳&lt;/strong&gt;，二是&lt;strong&gt;可以节省服务器的资源成本&lt;/strong&gt;。针对秒杀这一场景，削峰从本质上来说就是更多地延缓用户请求的发出，以便减少和过滤掉一些无效请求，它遵从“请求数要尽量少”的原则。&lt;/p&gt;
&lt;p&gt;流量削峰的一些操作思路：&lt;strong&gt;排队&lt;/strong&gt;、&lt;strong&gt;答题&lt;/strong&gt;、&lt;strong&gt;分层过滤&lt;/strong&gt;。这几种方式都是&lt;strong&gt;无损&lt;/strong&gt;（即不会损失用户的发出请求）的实现方案，当然还有些&lt;strong&gt;有损&lt;/strong&gt;的实现方案，比如&lt;strong&gt;限流&lt;/strong&gt;和&lt;strong&gt;机器负载保护&lt;/strong&gt;等一些强制措施也能达到削峰保护的目的，当然这都是不得已的一些措施。&lt;/p&gt;
&lt;h3 id=&#34;排队&#34;&gt;排队&lt;/h3&gt;
&lt;p&gt;要对流量进行削峰，最容易想到的解决方案就是用&lt;strong&gt;消息队列&lt;/strong&gt;来缓冲瞬时流量，把同步的直接调用转换成&lt;strong&gt;异步&lt;/strong&gt;的间接推送，中间通过一个队列在一端承接瞬时的流量洪峰，在另一端平滑地将消息推送出去。在这里，消息队列就像“水库”一样， 拦蓄上游的洪水，削减进入下游河道的洪峰流量，从而达到减免洪水灾害的目的。&lt;/p&gt;
&lt;p&gt;用消息队列来缓冲瞬时流量的方案，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/8MshEuyCFjSUq3g.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;但是，如果流量峰值持续一段时间达到了消息队列的处理上限，例如本机的消息积压达到了存储空间的上限，消息队列同样也会被压垮，这样虽然保护了下游的系统，但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时，即使是有水库恐怕也无济于事。&lt;/p&gt;
&lt;p&gt;除了消息队列，类似的&lt;strong&gt;排队方式&lt;/strong&gt;还有很多，例如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;利用&lt;strong&gt;线程池加锁等待&lt;/strong&gt;也是一种常用的排队方式；&lt;/li&gt;
&lt;li&gt;先进先出、先进后出等常用的&lt;strong&gt;内存排队算法&lt;/strong&gt;的实现方式；&lt;/li&gt;
&lt;li&gt;把&lt;strong&gt;请求序列化到文件中&lt;/strong&gt;，然后再顺序地读文件（例如&lt;strong&gt;基于 MySQL binlog 的同步机制&lt;/strong&gt;）来&lt;strong&gt;恢复请求&lt;/strong&gt;等方式。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到，这些方式都有一个共同特征，就是把“一步的操作”变成“两步的操作”，其中增加的一步操作用来起到缓冲的作用。&lt;/p&gt;
&lt;h3 id=&#34;答题&#34;&gt;答题&lt;/h3&gt;
&lt;p&gt;增加答题功能呢主要是为了增加购买的复杂度，从而达到两个目的：&lt;/p&gt;
&lt;p&gt;第一个目的是&lt;strong&gt;防止&lt;/strong&gt;部分买家使用秒杀器在参加秒杀时&lt;strong&gt;作弊&lt;/strong&gt;。2011 年秒杀非常火的时候，秒杀器也比较猖獗，因而没有达到全民参与和营销的目的，所以系统增加了答题来限制秒杀器。增加答题后，下单的时间基本控制在 2s 后，秒杀器的下单比例也大大下降。&lt;/p&gt;
&lt;p&gt;第二个目的其实就是&lt;strong&gt;延缓请求&lt;/strong&gt;，起到对请求流量进行削峰的作用，从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长，从以前的 1s 之内延长到 2s ~ 10s。这样一来，&lt;strong&gt;请求峰值基于时间分片了&lt;/strong&gt;。这个时间的分片对服务端处理并发非常重要，会大大减轻压力。而且，由于请求具有先后顺序，靠后的请求到来时自然也就没有库存了，因此根本到不了最后的下单步骤，所以真正的并发写就非常有限了。这种设计思路目前用得非常普遍，如当年支付宝的“咻一咻”、微信的“摇一摇”都是类似的方式。&lt;/p&gt;
&lt;p&gt;这里说一下秒杀答题的设计思路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/i1IdEKObBXea6YZ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，整个秒杀答题的逻辑主要分为 3 部分。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;题库生成模块&lt;/strong&gt;，这个部分主要就是生成一个个问题和答案，其实题目和答案本身并不需要很复杂，重要的是能够防止由机器来算出结果，即防止秒杀器来答题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;题库的推送模块&lt;/strong&gt;，用于在秒杀答题前，把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的，目的也是防止答题作弊。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;题目的图片生成模块&lt;/strong&gt;，用于把题目生成为图片格式，并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题，它要求只有人才能理解题目本身的含义。这里还要注意一点，由于答题时网络比较拥挤，我们应该把题目的图片&lt;strong&gt;提前推送到 CDN&lt;/strong&gt; 上并且要进行&lt;strong&gt;预热&lt;/strong&gt;，不然的话当用户真正请求题目时，图片可能加载比较慢，从而影响答题的体验。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实真正答题的逻辑比较简单，很好理解：当用户提交的答案和题目对应的答案做比较，如果通过了就继续进行下一步的下单逻辑，否则就失败。可以把问题和答案用下面这样的 key 来进行 MD5 加密：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题 key：userId + itemId + question_Id + time + PK&lt;/li&gt;
&lt;li&gt;答案 key：userId + itemId + answer + PK&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证的逻辑如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/5gX9NwfcPqKWOao.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;注意，这里面的验证逻辑，除了验证问题的答案以外，还包括用户本身身份的验证，例如是否已经登录、用户的 Cookie 是否完整、用户是否重复频繁提交等。&lt;/p&gt;
&lt;p&gt;除了做正确性验证，还可以对提交答案的时间做些限制，例如从开始答题到接受答案要超过 1s，因为小于 1s 是人为操作的可能性很小，这样也能防止机器答题的情况。&lt;/p&gt;
&lt;h3 id=&#34;分层过滤&#34;&gt;分层过滤&lt;/h3&gt;
&lt;p&gt;前面介绍的排队和答题要么是少发请求，要么对发出来的请求进行缓冲，而针对秒杀场景还有一种方法，就是对请求进行分层过滤，从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/r1nGDtkuNxH9KMZ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;假如请求分别经过 CDN、前台读系统（如商品详情系统）、后台系统（如交易系统）和数据库这几层，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大部分数据和流量在用户浏览器或者 CDN 上获取，这一层可以拦截大部分数据的读取；&lt;/li&gt;
&lt;li&gt;经过第二层（即前台系统）时数据（包括强一致性的数据）尽量得走 Cache，过滤一些无效的请求；&lt;/li&gt;
&lt;li&gt;再到第三层后台系统，主要做数据的二次检验，对系统做好保护和限流，这样数据量和请求就进一步减少；&lt;/li&gt;
&lt;li&gt;最后在数据层完成数据的强一致性校验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样就像漏斗一样，尽量把数据量和请求量一层一层地过滤和减少了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分层过滤的核心思想是：在不同的层次尽可能地过滤掉无效请求，让“漏斗”最末端的才是有效请求&lt;/strong&gt;。而要达到这种效果，就必须对数据做分层的校验。&lt;/p&gt;
&lt;p&gt;分层校验的基本原则是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将动态请求的读数据缓存（Cache）在 Web 端，过滤掉无效的数据读；&lt;/li&gt;
&lt;li&gt;对读数据不做强一致性校验，减少因为一致性校验产生瓶颈的问题；&lt;/li&gt;
&lt;li&gt;对写数据进行&lt;strong&gt;基于时间的合理分片&lt;/strong&gt;，过滤掉过期的失效请求；&lt;/li&gt;
&lt;li&gt;对写请求做限流保护，将超出系统承载能力的请求过滤掉；&lt;/li&gt;
&lt;li&gt;对写数据进行强一致性校验，只保留最后有效的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分层校验的目的是：在读系统中，尽量减少由于一致性校验带来的系统瓶颈，但是尽量将不影响性能的检查条件提前，如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等；在写数据系统中，主要对写的数据（如“库存”）做一致性检查，最后在数据库层保证数据的最终准确性（如“库存”不能减为负数）。&lt;/p&gt;
&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;削峰的 3 种处理方式：一个是通过队列来缓冲请求，即&lt;strong&gt;控制请求的发出&lt;/strong&gt;；一个是通过答题来&lt;strong&gt;延长请求发出的时间&lt;/strong&gt;，在请求发出后承接请求时进行控制，最后再对不符合条件的请求进行过滤；最后一种是&lt;strong&gt;对请求进行分层过滤&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;其中，队列缓冲方式更加通用，它适用于内部上下游系统之间调用请求不平缓的场景，由于内部系统的服务质量要求不能随意丢弃请求，所以使用消息队列能起到很好的削峰和缓冲作用。&lt;/p&gt;
&lt;p&gt;而答题更适用于秒杀或者营销活动等应用场景，在请求发起端就控制发起请求的速度，因为越到后面无效请求也会越多，所以配合后面介绍的分层拦截的方式，可以更进一步减少无效请求对系统资源的消耗。&lt;/p&gt;
&lt;p&gt;分层过滤非常适合交易性的写请求，比如减库存或者拼车这种场景，在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的，所以读的数据是否一定要非常准确呢？其实不一定，可以放一些请求过去，然后在真正减的时候再做强一致性保证，这样既过滤一些请求又解决了强一致性读的瓶颈。&lt;/p&gt;
&lt;p&gt;不过，在削峰的处理方式上除了采用技术手段，其实还可以&lt;strong&gt;采用业务手段&lt;/strong&gt;来达到一定效果，例如在零点开启大促的时候由于流量太大导致支付系统阻塞，这个时候可以采用发放优惠券、发起抽奖活动等方式，将一部分流量分散到其他地方，这样也能起到缓冲流量的作用。&lt;/p&gt;
&lt;h2 id=&#34;5-性能优化&#34;&gt;5. 性能优化&lt;/h2&gt;
&lt;p&gt;结合秒杀这一场景，重点介绍下服务端的一些优化技巧。&lt;/p&gt;
&lt;h3 id=&#34;影响性能的因素&#34;&gt;影响性能的因素&lt;/h3&gt;
&lt;p&gt;想要提升性能，首先肯定要知道哪些因素对于系统性能的影响最大，然后再针对这些具体的因素想办法做优化。&lt;/p&gt;
&lt;p&gt;那么，哪些因素对性能有影响呢？在回答这个问题之前，我们先定义一下“性能”，服务设备不同对性能的定义也是不一样的，例如 CPU 主要看主频、磁盘主要看 IOPS（Input/Output Operations Per Second，即每秒进行读写操作的次数）。&lt;/p&gt;
&lt;p&gt;而今天讨论的主要是系统服务端性能，一般用 &lt;strong&gt;QPS（Query Per Second，每秒请求数）&lt;strong&gt;来衡量，还有一个影响和 QPS 也息息相关，那就是&lt;/strong&gt;响应时间（Response Time，RT）&lt;/strong&gt;，它可以理解为服务器处理响应的耗时。&lt;/p&gt;
&lt;p&gt;正常情况下响应时间（RT）越短，一秒钟处理的请求数（QPS）自然也就会越多，这在单线程处理的情况下看起来是线性的关系，即我们只要把每个请求的响应时间降到最低，那么性能就会最高。&lt;/p&gt;
&lt;p&gt;但是响应时间总有一个极限，不可能无限下降，所以又出现了另外一个维度，即通过多线程，来处理请求。这样理论上就变成了“总 QPS =（1000ms / 响应时间）× 线程数量”，这样性能就和两个因素相关了，一个是一次响应的服务端耗时，一个是处理请求的线程数。&lt;/p&gt;
&lt;p&gt;接下来看看这个两个因素到底会造成什么样的影响。&lt;/p&gt;
&lt;h4 id=&#34;响应时间和-qps-的关系&#34;&gt;响应时间和 QPS 的关系&lt;/h4&gt;
&lt;p&gt;对于大部分的 Web 系统而言，响应时间一般都是由 CPU 执行时间和线程等待时间（比如 RPC、IO 等待、Sleep、Wait 等）组成，即服务器在处理一个请求时，一部分是 CPU 本身在做运算，还有一部分是在各种等待。&lt;/p&gt;
&lt;p&gt;理解了服务器处理请求的逻辑，那为什么我们不去减少这种等待时间。很遗憾，根据实际的测试发现，减少线程等待时间对提升性能的影响没有想象得那么大，它并不是线性的提升关系，这点在很多代理服务器（Proxy）上可以做验证。&lt;/p&gt;
&lt;p&gt;如果代理服务器本身没有 CPU 消耗，在每次给代理服务器代理的请求加个延时，即增加响应时间，但是这对代理服务器本身的吞吐量并没有多大的影响，因为代理服务器本身的资源并没有被消耗，可以通过增加代理服务器的处理线程数，来弥补响应时间对代理服务器的 QPS 的影响。&lt;/p&gt;
&lt;p&gt;其实，真正对性能有影响的是 CPU 的执行时间。这也很好理解，因为 CPU 的执行真正消耗了服务器的资源。经过实际的测试，如果减少 CPU 一半的执行时间，就可以增加一倍的 QPS。&lt;/p&gt;
&lt;p&gt;也就是说，应该致力于减少 CPU 的执行时间。&lt;/p&gt;
&lt;h4 id=&#34;线程数对-qps-的影响&#34;&gt;线程数对 QPS 的影响&lt;/h4&gt;
&lt;p&gt;单看“总 QPS”的计算公式，会觉得线程数越多 QPS 也就会越高，但这会一直正确吗？显然不是，线程数不是越多越好，因为线程本身也消耗资源，也受到其他因素的制约。例如，线程越多系统的线程切换成本就会越高，而且每个线程也都会耗费一定内存。&lt;/p&gt;
&lt;p&gt;那么，设置什么样的线程数最合理呢？其实&lt;strong&gt;很多多线程的场景都有一个默认配置，即“线程数 = 2 * CPU 核数 + 1”&lt;/strong&gt;。除去这个配置，还有一个根据最佳实践得出来的公式：&lt;/p&gt;
&lt;p&gt;线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量&lt;/p&gt;
&lt;p&gt;当然，最好的办法是通过性能测试来发现最佳的线程数。&lt;/p&gt;
&lt;p&gt;换句话说，要提升性能就要&lt;strong&gt;减少 CPU 的执行时间&lt;/strong&gt;，另外就是要&lt;strong&gt;设置一个合理的并发线程数&lt;/strong&gt;，通过这两方面来显著提升服务器的性能。&lt;/p&gt;
&lt;p&gt;现在知道了如何来快速提升性能，那应该怎么发现系统哪里最消耗 CPU 资源呢？&lt;/p&gt;
&lt;h3 id=&#34;如何发现瓶颈&#34;&gt;如何发现瓶颈&lt;/h3&gt;
&lt;p&gt;就服务器而言，会出现瓶颈的地方有很多，例如 CPU、内存、磁盘以及网络等都可能会导致瓶颈。此外，不同的系统对瓶颈的关注度也不一样，例如&lt;strong&gt;对缓存系统而言，制约它的是内存&lt;/strong&gt;，而&lt;strong&gt;对存储型系统来说 I/O 更容易是瓶颈&lt;/strong&gt;。&lt;strong&gt;秒杀场景它的瓶颈更多地发生在 CPU 上&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那么，如何发现 CPU 的瓶颈呢？其实有很多 CPU 诊断工具可以发现 CPU 的消耗，最常用的就是 JProfiler 和 Yourkit 这两个工具，它们可以列出整个请求中每个函数的 CPU 执行时间，可以发现哪个函数消耗的 CPU 时间最多，以便有针对性地做优化。&lt;/p&gt;
&lt;p&gt;当然还有一些办法也可以近似地统计 CPU 的耗时，例如通过 jstack 定时地打印调用栈，如果某些函数调用频繁或者耗时较多，那么那些函数就会多次出现在系统调用栈里，这样相当于采样的方式也能够发现耗时较多的函数。&lt;/p&gt;
&lt;p&gt;虽说秒杀系统的瓶颈大部分在 CPU，但这并不表示其他方面就一定不出现瓶颈。例如，如果海量请求涌过来，页面又比较大，那么网络就有可能出现瓶颈。&lt;/p&gt;
&lt;p&gt;怎样简单地判断 CPU 是不是瓶颈呢？一个办法就是看当 QPS 达到极限时，服务器的 CPU 使用率是不是超过了 95%，如果没有超过，那么表示 CPU 还有提升的空间，要么是有锁限制，要么是有过多的本地 I/O 等待发生。&lt;/p&gt;
&lt;h3 id=&#34;如何优化系统&#34;&gt;如何优化系统&lt;/h3&gt;
&lt;p&gt;对 Java 系统来说，可以优化的地方很多，这里重点说一下比较有效的几种手段，它们是：减少编码、减少序列化、Java 极致优化、并发读优化。&lt;/p&gt;
&lt;h4 id=&#34;减少编码&#34;&gt;减少编码&lt;/h4&gt;
&lt;p&gt;Java 的编码运行比较慢，这是 Java 的一大硬伤。在很多场景下，只要涉及字符串的操作（如输入输出操作、I/O 操作）都比较耗 CPU 资源，不管它是磁盘 I/O 还是网络 I/O，因为都需要&lt;strong&gt;将字符转换成字节&lt;/strong&gt;，而这个转换必须&lt;strong&gt;编码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每个字符的编码都需要查表，而这种查表的操作非常耗资源，所以减少字符到字节或者相反的转换、减少字符编码会非常有成效。减少编码就可以大大提升性能。&lt;/p&gt;
&lt;p&gt;那么如何才能减少编码呢？例如，网页输出是可以直接进行流输出的，即用 resp.getOutputStream() 函数写数据，把一些静态的数据提前转化成字节，等到真正往外写的时候再直接用 OutputStream() 函数写，就可以减少静态数据的编码转换。&lt;/p&gt;
&lt;h4 id=&#34;减少序列化&#34;&gt;减少序列化&lt;/h4&gt;
&lt;p&gt;序列化也是 Java 性能的一大天敌，减少 Java 中的序列化操作也能大大提升性能。又因为&lt;strong&gt;序列化往往是和编码同时发生的&lt;/strong&gt;，所以减少序列化也就减少了编码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;序列化大部分是在 RPC 中发生的&lt;/strong&gt;，因此避免或者减少 RPC 就可以减少序列化，当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案，就是可以将多个关联性比较强的应用进行“&lt;strong&gt;合并部署&lt;/strong&gt;”，而减少不同应用之间的 RPC 也可以减少序列化的消耗。&lt;/p&gt;
&lt;p&gt;所谓“合并部署”，就是把两个原本在不同机器上的不同应用合并部署到一台机器上，当然不仅仅是部署在一台机器上，还要在同一个 Tomcat 容器中，且不能走本机的 Socket，这样才能避免序列化的产生。&lt;/p&gt;
&lt;h4 id=&#34;java-极致优化&#34;&gt;Java 极致优化&lt;/h4&gt;
&lt;p&gt;Java 和通用的 Web 服务器（如 Nginx 或 Apache 服务器）相比，在处理大并发的 HTTP 请求时要弱一点，所以一般我们都会对大流量的 Web 系统做&lt;strong&gt;静态化改造&lt;/strong&gt;，让大部分请求和数据直接在 Nginx 服务器或者 Web 代理服务器（如 Varnish、Squid 等）上直接返回（这样可以&lt;strong&gt;减少数据的序列化与反序列化&lt;/strong&gt;），而 Java 层只需处理少量数据的动态请求。针对这些请求，可以使用以下手段进行优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接使用 Servlet 处理请求&lt;/strong&gt;。避免使用传统的 MVC 框架，这样可以绕过一大堆复杂且用处不大的处理逻辑，节省 1ms 时间（具体取决于你对 MVC 框架的依赖程度）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接输出流数据&lt;/strong&gt;。使用 resp.getOutputStream() 而不是 resp.getWriter() 函数，可以省掉一些不变字符数据的编码，从而提升性能；数据输出时&lt;strong&gt;推荐使用 JSON&lt;/strong&gt; 而不是**模板引擎（一般都是解释执行）**来输出页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;并发读优化&#34;&gt;并发读优化&lt;/h4&gt;
&lt;p&gt;也许有人会觉得这个问题很容易解决，无非就是放到 Tair 缓存里面。集中式缓存为了&lt;strong&gt;保证命中率&lt;/strong&gt;一般都会采用&lt;strong&gt;一致性 Hash&lt;/strong&gt;，所以同一个 key 会落到同一台机器上。虽然单台缓存机器也能支撑 30w/s 的请求，但还是远不足以应对像“大秒”这种级别的热点商品。那么，该如何彻底解决单点的瓶颈呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tair 是一个 Key / Value 结构数据的解决方案，它默认支持基于内存和文件的两种存储方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;答案是采用&lt;strong&gt;应用层的 LocalCache&lt;/strong&gt;，即在秒杀系统的&lt;strong&gt;单机上缓存&lt;/strong&gt;商品相关的数据。&lt;/p&gt;
&lt;p&gt;那么，又如何缓存（Cache）数据呢？需要划分成动态数据和静态数据分别进行处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;像商品中的“标题”和“描述”这些本身不变的数据，会在秒杀开始之前全量推送到秒杀机器上，并一直缓存到秒杀结束；&lt;/li&gt;
&lt;li&gt;像库存这类动态数据，会采用“&lt;strong&gt;被动失效&lt;/strong&gt;”的方式缓存一定时间（一般是数秒），失效后再去缓存拉取最新的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;像库存这种频繁更新的数据，一旦数据不一致，会不会导致超卖？&lt;/p&gt;
&lt;p&gt;这就要用到前面介绍的读数据的分层校验原则了，&lt;strong&gt;读的场景可以允许一定的脏数据&lt;/strong&gt;，因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存，可以等到真正写数据时再保证&lt;strong&gt;最终一致性&lt;/strong&gt;，通过在数据的&lt;strong&gt;高可用性&lt;/strong&gt;和&lt;strong&gt;一致性&lt;/strong&gt;之间的平衡，来解决高并发的数据读取问题。&lt;/p&gt;
&lt;h4 id=&#34;总结-1&#34;&gt;总结&lt;/h4&gt;
&lt;p&gt;性能优化的过程首先要从发现短板开始，除了介绍的一些优化措施外，还可以在减少数据、数据分级（动静分离），以及减少中间环节、增加预处理等这些环节上做优化。&lt;/p&gt;
&lt;p&gt;首先是“发现短板”，比如考虑以下因素的一些限制：光速（光速：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 等资源的限制）和应用本身的一些瓶颈等。&lt;/p&gt;
&lt;p&gt;其次是减少数据。事实上，有两个地方特别影响性能，一是服务端在处理数据时不可避免地存在字符到字节的相互转化，二是 &lt;strong&gt;HTTP 请求时要做 Gzip 压缩&lt;/strong&gt;，还有网络传输的耗时，这些都和数据大小密切相关。&lt;/p&gt;
&lt;p&gt;再次，就是数据分级，也就是要保证首屏为先、重要信息为先，次要信息则异步加载，以这种方式提升用户获取数据的体验。&lt;/p&gt;
&lt;p&gt;最后就是要减少中间环节，减少字符到字节的转换，增加预处理（提前做字符到字节的转换）去掉不需要的操作。&lt;/p&gt;
&lt;p&gt;此外，要做好优化，还需要做好应用基线，比如性能基线（何时性能突然下降）、成本基线（去年双 11 用了多少台机器）、链路基线（系统发生了哪些变化），可以通过这些基线持续关注系统的性能，做到在代码上提升编码质量，在业务上改掉不合理的调用，在架构和调用链路上不断的改进。&lt;/p&gt;
&lt;h2 id=&#34;6-减库存逻辑&#34;&gt;6. 减库存逻辑&lt;/h2&gt;
&lt;p&gt;系统是用户下单了就算这个商品卖出去了，还是等到用户真正付款了才算卖出了呢？这是个问题。&lt;/p&gt;
&lt;p&gt;可以先根据减库存是发生在下单阶段还是付款阶段，把减库存做一下划分。&lt;/p&gt;
&lt;h3 id=&#34;减库存有哪几种方式&#34;&gt;减库存有哪几种方式&lt;/h3&gt;
&lt;p&gt;在正常的电商平台购物场景中，用户的实际购买过程一般分为两步：下单和付款。你想买一台 iPhone 手机，在商品页面点了“立即购买”按钮，核对信息之后点击“提交订单”，这一步称为下单操作。下单之后，只有真正完成付款操作才能算真正购买，也就是俗话说的“落袋为安”。&lt;/p&gt;
&lt;p&gt;那如果你是架构师，你会在哪个环节完成减库存的操作呢？总结来说，减库存操作一般有如下几个方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下单减库存&lt;/strong&gt;，即当买家下单后，在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式，也是控制最精确的一种，下单时直接通过&lt;strong&gt;数据库的事务机制&lt;/strong&gt;控制商品库存，这样一定不会出现超卖的情况。但是要知道，有些人下完单可能并不会付款。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;付款减库存&lt;/strong&gt;，即买家下单后，并不立即减库存，而是等到有用户付款后才真正减库存，否则库存一直保留给其他买家。但因为付款时才减库存，&lt;strong&gt;如果并发比较高，有可能出现买家下单后付不了款的情况&lt;/strong&gt;，因为可能商品已经被其他人买走了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预扣库存&lt;/strong&gt;，这种方式相对复杂一些，买家下单后，库存为其保留一定的时间（如 10 分钟），超过这个时间，库存将会自动释放，释放后其他买家就可以继续购买。在买家付款前，系统会校验该订单的库存是否还有保留：如果没有保留，则再次尝试预扣；如果库存不足（也就是预扣失败）则不允许继续付款；如果预扣成功，则完成付款并实际地减去库存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上这几种减库存的方式都会存在一些问题。&lt;/p&gt;
&lt;h3 id=&#34;减库存可能存在的问题&#34;&gt;减库存可能存在的问题&lt;/h3&gt;
&lt;p&gt;由于购物过程中存在两步或者多步的操作，因此在不同的操作步骤中减库存，就会存在一些可能被恶意买家利用的漏洞，例如发生&lt;strong&gt;恶意下单&lt;/strong&gt;的情况。&lt;/p&gt;
&lt;p&gt;假如我们采用“下单减库存”的方式，即用户下单后就减去库存，正常情况下，买家下单后付款的概率会很高，所以不会有太大问题。但是有一种场景例外，就是当卖家参加某个活动时，此时活动的有效时间是商品的黄金售卖时间，如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单，让这款商品的库存减为零，那么这款商品就不能正常售卖了。要知道，这些恶意下单的人是不会真正付款的，这正是“下单减库存”方式的不足之处。&lt;/p&gt;
&lt;p&gt;既然“下单减库存”可能导致恶意下单，从而影响卖家的商品销售，那么有没有办法解决呢？你可能会想，采用“付款减库存”的方式是不是就可以了？的确可以。但是，“付款减库存”又会导致另外一个问题：&lt;strong&gt;库存超卖&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;假如有 100 件商品，就可能出现 300 人下单成功的情况，因为下单时不会减库存，所以也就可能出现下单成功数远远超过真正库存数的情况，这尤其会发生在做活动的热门商品上。这样一来，就会导致很多买家下单成功但是付不了款，买家的购物体验自然比较差。&lt;/p&gt;
&lt;p&gt;可以看到，不管是“下单减库存”还是“付款减库存”，都会导致商品库存不能完全和实际售卖情况对应起来的情况。&lt;/p&gt;
&lt;p&gt;那么，既然“下单减库存”和“付款减库存”都有缺点，能否把两者相结合，将两次操作进行前后关联起来，下单时先预扣，在规定时间内不付款再释放库存，即采用“&lt;strong&gt;预扣库存&lt;/strong&gt;”这种方式呢？&lt;/p&gt;
&lt;p&gt;这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢？其实没有！针对恶意下单这种情况，虽然把有效的付款时间设置为 10 分钟，但是恶意买家完全可以在 10 分钟后再次下单，或者采用一次下单很多件的方式把库存减完。针对这种情况，解决办法还是要结合安全和反作弊的措施来制止。&lt;/p&gt;
&lt;p&gt;例如，给经常下单不付款的买家进行识别打标（可以在被打标的买家下单时不减库存）、给某些类目设置最大购买件数（例如，参加活动的商品一人最多只能买 3 件），以及对重复下单不付款的操作进行次数限制等。&lt;/p&gt;
&lt;p&gt;针对“库存超卖”这种情况，在 10 分钟时间内下单的数量仍然有可能超过库存数量，遇到这种情况我们只能区别对待：对普通的商品下单数量超过库存数量的情况，可以通过补货来解决；但是有些卖家完全不允许库存为负数的情况，那只能在买家付款时提示库存不足。&lt;/p&gt;
&lt;h3 id=&#34;大型秒杀中如何减库存&#34;&gt;大型秒杀中如何减库存？&lt;/h3&gt;
&lt;p&gt;目前来看，&lt;strong&gt;业务系统中最常见的就是预扣库存方案&lt;/strong&gt;，像你在买机票、买电影票时，下单后一般都有个“&lt;strong&gt;有效付款时间&lt;/strong&gt;”，超过这个时间订单自动释放，这都是典型的预扣库存方案。而具体到秒杀这个场景，应该采用哪种方案比较好呢？&lt;/p&gt;
&lt;p&gt;由于参加秒杀的商品，一般都是“抢到就是赚到”，所以成功下单后却不付款的情况比较少，再加上卖家对秒杀商品的库存有严格限制，所以&lt;strong&gt;秒杀商品采用“下单减库存”更加合理&lt;/strong&gt;。另外，理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在&lt;strong&gt;逻辑上更为简单&lt;/strong&gt;，所以&lt;strong&gt;性能上更占优势&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;“下单减库存”在数据一致性上，主要就是保证大并发请求时库存数据不能为负数，也就是要保证数据库中的库存字段值不能为负数，一般我们有多种解决方案：&lt;/p&gt;
&lt;p&gt;一种是在应用程序中通过事务来判断，即保证减后库存不能为负数，否则就回滚；另一种办法是直接设置数据库的字段数据为无符号整数，这样减后库存字段值小于零时会直接执行 SQL 语句来报错；再有一种就是使用 CASE WHEN 判断语句，例如这样的 SQL 语句：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CASE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;xxx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;THEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;xxx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ELSE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;END&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：上述语句必须在&lt;strong&gt;串行隔离&lt;/strong&gt;级别才能用，否则就会超卖。&lt;/p&gt;
&lt;h3 id=&#34;秒杀减库存的极致优化&#34;&gt;秒杀减库存的极致优化&lt;/h3&gt;
&lt;p&gt;在交易环节中，“库存”是个关键数据，也是个热点数据，因为交易的各个环节中都可能涉及对库存的查询。但是，在前面介绍分层过滤时提到过，&lt;strong&gt;秒杀中并不需要对库存有精确的一致性读&lt;/strong&gt;，把库存数据放到缓存（Cache）中，可以大大提升读性能。&lt;/p&gt;
&lt;p&gt;解决大并发读问题，可以采用 &lt;strong&gt;LocalCache&lt;/strong&gt;（即在秒杀系统的单机上缓存商品相关的数据）和对数据进行分层过滤的方式，但是像减库存这种&lt;strong&gt;大并发写&lt;/strong&gt;无论如何还是避免不了，这也是秒杀场景下最为核心的一个技术难题。&lt;/p&gt;
&lt;p&gt;因此这里专门来说一下秒杀场景下减库存的极致优化思路，包括如何在缓存中减库存以及如何在数据库中减库存。&lt;/p&gt;
&lt;p&gt;秒杀商品和普通商品的减库存还是有些差异的，例如商品数量比较少，交易时间段也比较短，因此这里有一个大胆的假设，即能否&lt;strong&gt;把秒杀商品减库存直接放到缓存系统中实现&lt;/strong&gt;，也就是直接在缓存中减库存或者在一个带有持久化功能的缓存系统（如 Redis）中完成呢？&lt;/p&gt;
&lt;p&gt;如果你的秒杀商品的减库存逻辑非常单一，比如没有复杂的 SKU 库存和总库存这种联动关系的话，完全可以。但是如果有比较复杂的减库存逻辑，或者需要使用事务，还是必须在数据库中完成减库存。&lt;/p&gt;
&lt;p&gt;由于 MySQL 存储数据的特点，同一数据在数据库里肯定是一行存储（MySQL），因此会有大量线程来竞争 &lt;strong&gt;InnoDB 行锁&lt;/strong&gt;，而并发度越高时等待线程会越多，**TPS（Transaction Per Second，即每秒处理的消息数）**会下降，**响应时间（RT）**会上升，数据库的吞吐量就会严重受影响。&lt;/p&gt;
&lt;p&gt;这就可能引发一个问题，就是单个热点商品会影响整个数据库的性能， 导致 0.01% 的商品影响 99.99% 的商品的售卖，这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行&lt;strong&gt;隔离&lt;/strong&gt;，把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦，比如要做热点数据的动态迁移以及单独的数据库等。&lt;/p&gt;
&lt;p&gt;而分离热点商品到单独的数据库还是没有解决&lt;strong&gt;并发锁&lt;/strong&gt;的问题，应该怎么办呢？要解决并发锁的问题，有两种办法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应用层做排队&lt;/strong&gt;。按照商品维度设置队列顺序执行，这样能减少同一台机器对数据库同一行记录进行操作的并发度，同时也能控制单个商品占用数据库连接的数量，防止热点商品占用太多的数据库连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库层做排队&lt;/strong&gt;。应用层只能做到单机的排队，但是应用机器数本身很多，这种排队方式控制并发的能力仍然有限，所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种 MySQL 的 InnoDB 层上的补丁程序（patch），可以在数据库层上&lt;strong&gt;对单行记录做到并发排队&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;“按照商品维度设置队列顺序执行”的意思就是，为了防止同一个商品对数据库的操作占用太多的数据库资源，所以采用队列的方式，让其他商品也有公平的机会得到数据的响应，例如如果秒杀的时候，秒杀商品肯定占用大量的请求，数据库的连接池有可能都被秒杀商品占用了，如果不做队列的话，那么其他商品就得不到数据库执行机会了。加入我们分10个队列，那么秒杀商品就会落在这10个队列中的一个，那么最多也就占用机器10分之一的资源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;排队和锁竞争不都是要等待吗，有什么区别？&lt;/p&gt;
&lt;p&gt;如果熟悉 MySQL 的话，会知道 InnoDB 内部的死锁检测，以及 MySQL Server 和 InnoDB 的切换会比较消耗性能，淘宝的 MySQL 核心团队还做了很多其他方面的优化，如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的补丁程序，配合在 SQL 里面加提示（hint），在事务里不需要等待应用层提交（COMMIT），而在数据执行完最后一条 SQL 后，直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚，可以减少网络等待时间（平均约 0.7ms）。&lt;/p&gt;
&lt;p&gt;另外，数据更新问题除了前面介绍的热点隔离和排队处理之外，还有些场景（如对商品的 lastmodifytime 字段的）更新会非常频繁，在某些场景下这些&lt;strong&gt;多条 SQL 是可以合并&lt;/strong&gt;的，一定时间内只要执行最后一条 SQL 就行了，以便减少对数据库的更新操作。&lt;/p&gt;
&lt;h2 id=&#34;7-plan-b兜底方案&#34;&gt;7. Plan B：兜底方案&lt;/h2&gt;
&lt;p&gt;为了保证系统的&lt;strong&gt;高可用&lt;/strong&gt;，必须设计一个 Plan B 方案来兜底，这样在最坏情况发生时仍然能够从容应对。&lt;/p&gt;
&lt;h3 id=&#34;高可用建设应该从哪里着手&#34;&gt;高可用建设应该从哪里着手&lt;/h3&gt;
&lt;p&gt;说到系统的高可用建设，它其实是一个系统工程，需要考虑到系统建设的各个阶段，也就是说它其实贯穿了系统建设的整个生命周期，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/UkoHtAJ7ewsx6jK.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;具体来说，系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段，以及故障发生时。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;架构阶段&lt;/strong&gt;：架构阶段主要考虑系统的&lt;strong&gt;可扩展性&lt;/strong&gt;和&lt;strong&gt;容错性&lt;/strong&gt;，要避免系统出现&lt;strong&gt;单点问题&lt;/strong&gt;。例如&lt;strong&gt;多机房单元化部署&lt;/strong&gt;，即使某个城市的某个机房出现整体故障，仍然不会影响整体网站的运转。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编码阶段&lt;/strong&gt;：编码最重要的是保证代码的健壮性，例如涉及远程调用问题时，要设置合理的&lt;strong&gt;超时退出机制&lt;/strong&gt;，防止被其他系统拖垮，也要对调用的返回结果集有预期，防止返回的结果超出程序处理范围，&lt;strong&gt;最常见的做法就是对错误异常进行捕获，对无法预料的错误要有默认处理结果&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试阶段&lt;/strong&gt;：测试主要是保证测试用例的覆盖度，保证最坏情况发生时，也有相应的处理流程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布阶段&lt;/strong&gt;：发布时也有一些地方需要注意，因为发布时最容易出现错误，因此要有&lt;strong&gt;紧急回滚机制&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行阶段&lt;/strong&gt;：运行时是系统的常态，系统大部分时间都会处于运行态，运行态最重要的是对系统的监控要准确及时，发现问题能够准确报警并且报警数据要准确详细，以便于排查问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障发生&lt;/strong&gt;：故障发生时首先最重要的就是及时止损，例如由于程序问题导致商品价格错误，那就要及时下架商品或者关闭购买链接，防止造成重大资产损失。然后就是要能够及时恢复服务，并定位原因解决问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为什么系统的高可用建设要放到整个生命周期中全面考虑？因为我们在每个环节中都可能犯错，而有些环节犯的错，在后面是无法弥补的。例如在架构阶段没有消除单点问题，那么系统上线后，遇到突发流量把单点给挂了，就只能干瞪眼，有时候想加机器都加不进去。所以高可用建设是一个系统工程，必须在每个环节都做好。&lt;/p&gt;
&lt;p&gt;那么针对秒杀系统，重点介绍在遇到大流量时，应该从哪些方面来保障系统的稳定运行，所以更多的是看如何针对&lt;strong&gt;运行阶段&lt;/strong&gt;进行处理，这就引出了接下来的内容：&lt;strong&gt;降级&lt;/strong&gt;、&lt;strong&gt;限流&lt;/strong&gt;和&lt;strong&gt;拒绝服务&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;降级&#34;&gt;降级&lt;/h3&gt;
&lt;p&gt;所谓“降级”，就是当系统的容量达到一定程度时，&lt;strong&gt;限制或者关闭系统的某些非核心功能&lt;/strong&gt;，从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程，所以对降级一般需要有一套预案来配合执行。如果把它系统化，就可以通过预案系统和开关系统来实现降级。&lt;/p&gt;
&lt;p&gt;降级方案可以这样设计：当秒杀流量达到 5w/s 时，把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现，也就是设置一个能够从&lt;strong&gt;开关系统&lt;/strong&gt;动态获取的系统参数。&lt;/p&gt;
&lt;p&gt;下图是开关系统的示意图。它分为两部分，一部分是开关控制台，它保存了开关的具体配置信息，以及具体执行开关所对应的机器列表；另一部分是执行下发开关数据的 Agent，主要任务就是保证开关被正确执行，即使系统重启后也会生效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/RBJnOL1XjClYgNu.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;执行降级无疑是在系统性能和用户体验之间选择了前者，降级后肯定会影响一部分用户的体验，例如在双 11 零点时，如果优惠券系统扛不住，可能会临时降级商品详情的优惠信息展示，把有限的系统资源用在保障交易系统正确展示优惠信息上，即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定，是一个不得已而为之的举措。&lt;/p&gt;
&lt;h3 id=&#34;限流&#34;&gt;限流&lt;/h3&gt;
&lt;p&gt;如果说降级是牺牲了一部分次要的功能和用户的体验效果，那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时，需要通过限制一部分流量来保护系统，并做到既可以人工执行开关，也支持自动化保护的措施。&lt;/p&gt;
&lt;p&gt;下图是限流系统的示意图。总体来说，限流既可以是在客户端限流，也可以是在服务端限流。此外，限流的实现方式既要支持 URL 以及方法级别的限流，也要支持基于 QPS 和线程的限流。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;客户端和服务端限流是针对 rpc 调用来说的，发起方可以理解为客户端，调用方可以理解为服务端。&lt;/p&gt;
&lt;p&gt;QPS，Queries-per-second，每秒查询率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以内部的系统调用为例，来分别说下客户端限流和服务端限流的优缺点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端限流&lt;/strong&gt;，好处可以限制请求的发出，通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时，没法设置合理的限流阈值：如果阈值设的太小，会导致服务端没有达到瓶颈时客户端已经被限制；而如果设的太大，则起不到限制的作用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端限流&lt;/strong&gt;，好处是可以根据服务端的性能设置合理的阈值，而缺点就是被限制的请求都是无效的请求，处理这些无效的请求本身也会消耗服务器资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/Yo38vDIal5XfRui.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在限流的实现手段上来讲，&lt;strong&gt;基于 QPS 和线程数的限流应用最多&lt;/strong&gt;，最大 QPS 很容易通过压测提前获取，例如我们的系统最高支持 1w QPS 时，可以设置 8000 来进行限流保护。线程数限流在客户端比较有效，例如在远程调用时设置连接池的线程数，超出这个并发线程请求，就将线程进行&lt;strong&gt;排队&lt;/strong&gt;或者&lt;strong&gt;直接超时丢弃&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;限流无疑会影响用户的正常请求，所以&lt;strong&gt;必然会导致一部分用户请求失败&lt;/strong&gt;，因此在系统处理这种异常时一定要设置&lt;strong&gt;超时时间&lt;/strong&gt;，防止因被限流的请求不能 **fast fail（快速失败）**而拖垮系统。&lt;/p&gt;
&lt;h3 id=&#34;拒绝服务&#34;&gt;拒绝服务&lt;/h3&gt;
&lt;p&gt;如果限流还不能解决问题，最后一招就是直接拒绝服务了。&lt;/p&gt;
&lt;p&gt;当系统负载达到一定阈值时，例如 CPU 使用率达到 90% 或者系统 load 值达到 2 * CPU 核数时，系统直接拒绝所有请求，这种方式是最暴力但也最有效的系统保护方式。例如秒杀系统在如下几个环节设计过载保护：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在最前端的 Nginx 上设置过载保护，当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码，在 Java 层同样也可以设计过载保护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;拒绝服务可以说是一种不得已的兜底方案，用以防止最坏情况发生，防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务，但是系统仍然可以运作，当负载下降时又很容易恢复，所以每个系统和每个环节都应该设置这个兜底方案，对系统做最坏情况下的保护。&lt;/p&gt;
&lt;h3 id=&#34;总结-2&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;网站的高可用建设是基础，可以说要深入到各个环节，更要长期规划并进行体系化建设，要在预防（建立常态的压力体系，例如上线前的单机压测到上线后的全链路压测）、管控（做好线上运行时的降级、限流和兜底保护）、监控（建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系，发现问题及时预警）和恢复体系（遇到故障要及时止损，并提供快速的数据订正工具等）等这些地方加强建设，每一个环节可能都有很多事情要做。&lt;/p&gt;
&lt;p&gt;另外，要保证高可用建设的落实，不仅要做系统建设，还要在组织上做好保障。高可用其实就是在说“稳定性”。稳定性是一个平时不重要，但真出了问题就会要命的事儿，所以很可能平时业务发展良好，稳定性建设就会给业务让路，相关的稳定性负责人员平时根本得不到重视，一旦遇到故障却又成了“背锅侠”。&lt;/p&gt;
&lt;p&gt;而要防止出现这种情况，就必须在组织上有所保障，例如可以让业务负责人背上稳定性 KPI 考核指标，然后在技术部门中建立稳定性建设小组，小组成员由每个业务线的核心力量兼任，他们的 KPI 由稳定性负责人来打分，这样稳定性小组就可以把一些体系化的建设任务落实到具体的业务系统中了。&lt;/p&gt;
&lt;h2 id=&#34;8-一些问题&#34;&gt;8. 一些问题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. “06 | 秒杀系统‘减库存’设计的核心逻辑”一文中，很多用户比较关注应用层排队的问题，大家主要的疑问就是应用层用队列接受请求，然后结果怎么返回的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实我这里所说的排队，更多地是说在服务端的服务调用之间采用排队的策略。例如，秒杀需要调用商品服务、调用价格优惠服务或者是创建订单服务，由于调用这些服务出现性能瓶颈，或者由于热点请求过于集中导致远程调用的连接数都被热点请求占据，那么那些正常的商品请求（非秒杀商品）就得不到服务器的资源了，这样对整个网站来说是不公平的。&lt;/p&gt;
&lt;p&gt;再比如说，正常整个网站上每秒只有几万个请求，这几万个请求可能是非常分散的，那么假如现在有一个秒杀商品，这个秒杀商品带来的瞬间请求一下子就打满了我们的服务器资源，这样就会导致那些正常的几万个请求得不到正常的服务，这个情况对系统来说是绝对不合理的，也是应该避免的。&lt;/p&gt;
&lt;p&gt;所以我们设计了一些策略，把秒杀系统独立出来，部署单独的一些服务器，也隔离了一些热点的数据库，等等。但是实际上不能把整个秒杀系统涉及的所有系统都独立部署一套，不然这样代价太大。&lt;/p&gt;
&lt;p&gt;既然不能所有系统都独立部署一套，势必就会存在一部分系统不能区分秒杀请求和正常请求，那么要如何防止前面所说的问题出现呢？通常的解决方案就是在部分服务调用的地方对请求进行 Hash 分组，来限制一部分热点请求过多地占用服务器资源，分组的策略就可以根据商品 ID 来进行 Hash，热点商品的请求始终会进入一个分组中，这样就解决了前面的问题。&lt;/p&gt;
&lt;p&gt;我看问的问题很多是说对秒杀的请求进行排队如何把结果通知给用户，我并不是说在用户 HTTP 请求时采用排队的策略（也就是把用户的所有秒杀请求都放到一个队列进行排队，然后在队列里按照进入队列的顺序进行选择，先到先得），虽然这看起来还是一个挺合理的设计，但是实际上并没有必要这么做！&lt;/p&gt;
&lt;p&gt;为什么？因为我们服务端接受请求本身就是按照请求顺序处理的，而且这个处理在 Web 层是实时同步的，处理的结果也会立马就返回给用户。但是我前面也说了，整个请求的处理涉及很多服务调用也涉及很多其他的系统，也会有部分的处理需要排队，所以可能有部分先到的请求由于后面的一些排队的服务拖慢，导致最终整个请求处理完成的时间反而比较后面的请求慢的情况。&lt;/p&gt;
&lt;p&gt;这种情况理论上的确存在，你可能会说这样可能会不公平，但是这的确没有办法，这种所谓的“不公平”，并不是由于人为设置的因素导致的。&lt;/p&gt;
&lt;p&gt;你可能会问（如果你一定要问），采用请求队列的方式能不能做？我会说“能”，但是有两点问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是体验会比较差，因为是异步的方式，在页面中搞个倒计时，处理的时间会长一点；&lt;/li&gt;
&lt;li&gt;二是如果是根据入队列的时间来判断谁获得秒杀商品，那也太没有意思了，没有运气成分不也就没有惊喜了？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于大家在纠结异步请求如何返回结果的问题，其实有多种方案。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是页面中采用轮询的方式定时主动去服务端查询结果，例如每秒请求一次服务端看看有没有处理结果（现在很多支付页面都采用了这种策略），这种方式的缺点是服务端的请求数会增加不少。&lt;/li&gt;
&lt;li&gt;二是采用主动 push 的方式，这种就要求服务端和客户端保持连接了，服务端处理完请求主动 push 给客户端，这种方式的缺点是服务端的连接数会比较多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个问题，就是如果异步的请求失败了，怎么办？对秒杀来说，我觉得如果失败了直接丢弃就好了，最坏的结果就是这个人没有抢到而已。但是你非要纠结的话，就要做异步消息的持久化以及重试机制了，要保证异步请求的最终正确处理一般都要借助消息系统，即消息的最终可达，例如阿里的消息中间件是能承诺只要客户端消息发送成功，那么消息系统一定会保证消息最终被送到目的地，即消息不会丢。因为客户端只要成功发送一条消息，下游消费方就一定会消费这条消息，所以也就不存在消息发送失败的问题了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 在“02 | 如何才能做好动静分离？有哪些方案可选？”一文中，有介绍静态化的方案中关于 Hash 分组的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大家可能通常理解 Hash 分组，像 Cache 这种可能一个 key 对应的数据只存在于一个实例中，这样做其实是为了保证缓存命中率，因为所有请求都被路由到一个缓存实例中，除了第一次没有命中外，后面的都会命中。&lt;/p&gt;
&lt;p&gt;但是这样也存在一个问题，就是如果热点商品过于集中，Cache 就会成为瓶颈，这时单个实例也支撑不了。像秒杀这个场景中，单个商品对 Cache 的访问会超过 20w 次，一般单 Cache 实例都扛不住这么大的请求量。所以需要采用一个分组中有多个实例缓存相同的数据（冗余）的办法来支撑更大的访问量。&lt;/p&gt;
&lt;p&gt;你可能会问：一个商品数据存储在多个 Cache 实例中，如何保证数据一致性呢？（关于失效问题大家问得也比较多，后面再回答。）这个专栏中提的 Hash 分组都是基于 Nginx+Varnish 实现的，Nginx 把请求的 URL 中的商品 ID 进行 Hash 并路由到一个 upstream 中，这个 upstream 挂载一个 Varnish 分组（如下图所示）。这样，一个相同的商品就可以随机访问一个分组的任意一台 Varnish 机器了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/iwzv1lULr6pQjEa.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;另外一个问题，关于 Hash 分组大家关注比较多的是命中率的问题，就是 Cache 机器越多命中率会越低。&lt;/p&gt;
&lt;p&gt;这个其实很好理解，Cache 实例越多，那么这些 Cache 缓存数据需要访问的次数也就越多。例如我有 3 个 Redis 实例，需要 3 个 Redis 实例都缓存商品 A，那么至少需要访问 3 次才行，而且是这 3 次访问刚好落到不同的 Redis 实例中。那么从第 4 次访问开始才会被命中，如果仅仅是一个 Redis 实例，那么第二次访问时其实就能命中了。所以理论上 Cache 实例多会影响命中率。&lt;/p&gt;
&lt;p&gt;你可能还会问，如果访问量足够大，那么只是影响前几次命中率而已，是的，如果 Cache 一直不失效的话是这样的，但是在实际的生产环境中 Cache 失效是很频繁发生的事情。很多情况下，还没等到所有 Cache 实例填满，该商品就已经失效了。所以，我们要根据商品的重复访问量来合理地设置 Cache 分组。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 在“02 | 如何才能做好动静分离？有哪些方案可选？”和“04 | 流量削峰这事应该怎么做？”两篇文章中，关于 Cache 失效的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，咱们要有个共识，有 Cache 的地方就必然存在失效问题。为啥要失效？因为要保证数据的一致性。所以要用到 Cache 必然会问如何保证 Cache 和 DB 的数据一致性，如果 Cache 有分组的话，还要保证一个分组中多个实例之间数据的一致性，就像保证 MySQL 的主从一致一样。&lt;/p&gt;
&lt;p&gt;其实，失效有主动失效和被动失效两种方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;被动失效&lt;/strong&gt;，主要处理如模板变更和一些对时效性不太敏感数据的失效，采用&lt;strong&gt;设置一定时间长度&lt;/strong&gt;（如只缓存 3 秒钟）这种&lt;strong&gt;自动失效&lt;/strong&gt;的方式。当然，你也要开发一个后台管理界面，以便能够在紧急情况下手工失效某些 Cache。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动失效&lt;/strong&gt;，一般有 Cache 失效中心监控数据库表变化发送失效请求、系统发布也需要清空 Cache 数据等几种场景。其中失效中心承担了主要的失效功能，这个失效中心的逻辑图如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/nt8DjHQf5OJ9oze.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;失效中心会监控关键数据表的变更（有个中间件来解析 MySQL 的 binglog，然后发现有 Insert、Update、Delete 等操作时，会把变更前的数据以及要变更的数据转成一个消息发送给订阅方），通过这种方式来发送失效请求给 Cache，从而清除 Cache 数据。如果 Cache 数据放在 CDN 上，那么也可以采用类似的方式来设计级联的失效结构，采用主动发请求给 Cache 软件失效的方式，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/XPkevyN8zx2qrS7.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;这种失效由失效中心将失效请求发送给每个 CDN 节点上的 Console 机，然后 Console 机来发送失效请求给每台 Cache 机器。&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
