<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>复制 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%A4%8D%E5%88%B6/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 08 Aug 2023 10:40:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%A4%8D%E5%88%B6/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>《设计数据密集型应用》学习总结：存量、流量、回路——每个设计决策都是三者的权衡</title>
      <link>https://ftmao.com/blog/bookstore/ddia-book-notes/</link>
      <pubDate>Tue, 08 Aug 2023 10:40:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/ddia-book-notes/</guid>
      <description>&lt;h1 id=&#34;设计数据密集型应用学习总结存量流量回路每个设计决策都是三者的权衡&#34;&gt;《设计数据密集型应用》学习总结：存量、流量、回路——每个设计决策都是三者的权衡&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：写过 CRUD、用过 MySQL/Redis/Kafka，但说不清它们&amp;quot;为什么这么设计&amp;quot;的工程师。&lt;br&gt;
参考书籍：Martin Kleppmann《Designing Data-Intensive Applications》（DDIA，那只野猪书）&lt;br&gt;
阅读目标：不背目录，而是拿到一个能套在任何存储/中间件上的分析框架——存量、流量、回路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-写在前面别背组件背权衡&#34;&gt;0. 写在前面：别背组件，背权衡&lt;/h2&gt;
&lt;p&gt;市面上的&amp;quot;中间件教程&amp;quot;有个通病：教你 MySQL 怎么用、Redis 怎么用、Kafka 怎么用，但从不告诉你&lt;strong&gt;为什么它们长得这么不一样&lt;/strong&gt;。同样是存数据，Redis 敢把数据放内存，MySQL 不敢；同样是日志，Kafka 能当数据库用，而大多数人只拿它当管道。&lt;/p&gt;
&lt;p&gt;DDIA 这本书最值钱的地方，就是把这些&amp;quot;为什么&amp;quot;串成了一条线。读完我把它提炼成三个词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存量（State）&lt;/strong&gt;：数据怎么&lt;strong&gt;存&lt;/strong&gt;——存储引擎（LSM/B-Tree）、编码格式、行存列存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量（Flow）&lt;/strong&gt;：数据怎么&lt;strong&gt;动&lt;/strong&gt;——复制、分区、派生数据、流处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回路（Loop）&lt;/strong&gt;：系统怎么&lt;strong&gt;自我纠错&lt;/strong&gt;——事务、隔离级别、共识、线性一致性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话理解：&lt;strong&gt;任何一个数据系统的设计决策，都是在存量成本、流量吞吐、回路强度三者之间做取舍。&lt;/strong&gt; 你要强一致（回路强），就得牺牲可用性或延迟（流量贵）；你要写入快（流量大），就得接受读时合并或后台压缩的代价（存量结构变复杂）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 0&lt;/strong&gt;：读完这本书最大的误区是&amp;quot;上来就选最强的一致性&amp;quot;。Kleppmann 自己说过大意如此的话：一致性模型的选择应该由&lt;strong&gt;业务能不能容忍异常&lt;/strong&gt;决定，而不是由工程师的洁癖决定。能容忍就用最终一致，性能白捡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;和博客已有的 Redis/MySQL/MQ 笔记是&amp;quot;原理 vs 用法&amp;quot;的关系：那些笔记讲&amp;quot;这个命令怎么用、这个参数怎么调&amp;quot;，这篇讲&amp;quot;这个组件为什么被设计成这样&amp;quot;。建议搭配阅读——看完这篇再回头看 Redis 的 RDB/AOF、MySQL 的 InnoDB redo log、Kafka 的副本协议，会有&amp;quot;原来如此&amp;quot;的感觉。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一部分-存量数据怎么存&#34;&gt;第一部分 存量：数据怎么存&lt;/h2&gt;
&lt;h3 id=&#34;1-lsm-tree-vs-b-tree写的快和读的快不可兼得&#34;&gt;1. LSM-Tree vs B-Tree：写的快和读的快，不可兼得&lt;/h3&gt;
&lt;p&gt;数据库索引结构就两大流派，区别一句话：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B-Tree&lt;/strong&gt;：读的时候顺藤摸瓜，树的高度就是磁盘寻道次数。写入是&amp;quot;就地更新&amp;quot;——找到那一页，改掉。MySQL InnoDB 就是这个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LSM-Tree&lt;/strong&gt;：写入只往内存里的有序结构（Memtable）追加，内存写满了就整体刷到磁盘变成一个 &lt;strong&gt;SSTable&lt;/strong&gt;（Sorted String Table，一段有序的文件）。读取时从最新的 SSTable 往最旧的依次找。LevelDB、RocksDB、Cassandra、HBase 都是这个。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SSTable 合并（compaction）是 LSM 的灵魂&lt;/strong&gt;。因为同一个 key 的新旧版本散落在多个 SSTable 里，后台必须定期把它们归并排序、去重、丢掉被删除的墓碑标记，否则文件越积越多，读放大失控。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    W[&amp;#34;写入请求&amp;#34;] --&amp;gt; L[&amp;#34;WAL 追加日志&amp;lt;br/&amp;gt;（崩溃恢复用）&amp;#34;]
    L --&amp;gt; M[&amp;#34;Memtable&amp;lt;br/&amp;gt;（内存有序表）&amp;#34;]
    M --&amp;gt;|&amp;#34;写满阈值&amp;#34;| S1[&amp;#34;SSTable 1&amp;lt;br/&amp;gt;（不可变文件）&amp;#34;]
    M --&amp;gt;|&amp;#34;再写满&amp;#34;| S2[&amp;#34;SSTable 2&amp;#34;]
    M --&amp;gt;|&amp;#34;又写满&amp;#34;| S3[&amp;#34;SSTable 3&amp;#34;]
    S1 --&amp;gt; C[&amp;#34;后台 Compaction&amp;lt;br/&amp;gt;归并排序 + 去重 + 清墓碑&amp;#34;]
    S2 --&amp;gt; C
    S3 --&amp;gt; C
    C --&amp;gt; S4[&amp;#34;合并后的大 SSTable&amp;lt;br/&amp;gt;（旧文件丢弃）&amp;#34;]
    R[&amp;#34;读取请求&amp;#34;] --&amp;gt; M
    R --&amp;gt; S3
    R --&amp;gt; S2
    R --&amp;gt; S1
    R --&amp;gt;|&amp;#34;配合布隆过滤器&amp;lt;br/&amp;gt;跳过不含该 key 的文件&amp;#34;| MISS[&amp;#34;快速判不存在&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;小数字例子感受写放大：假设每写 1 GB 数据，经过 3 层 compaction，每层都要重写一遍，实际磁盘写入可能是 5~10 GB。这就是&amp;quot;写放大&amp;quot;，也是 RocksDB 调优的核心战场。&lt;/p&gt;
&lt;p&gt;LSM 读取慢怎么补救？靠&lt;strong&gt;布隆过滤器（Bloom filter）&lt;/strong&gt;：一个很小的内存位图，能告诉你&amp;quot;这个 key 肯定不在这个 SSTable 里&amp;quot;（可能有误报，但绝不漏报）。查一个不存在的 key 时，布隆过滤器直接拦掉，不用真的去翻磁盘文件——这对&amp;quot;查缓存未命中再查库&amp;quot;的场景是救命优化。Cassandra、HBase、RocksDB 全都内置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;怎么选？&lt;/strong&gt; 记住这张对照表：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;维度&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;B-Tree&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;LSM-Tree&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;写入&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;就地更新，写放大相对低但随机 IO&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顺序追加，吞吐高但有 compaction 写放大&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;读取&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;稳定，一次查找几层树&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;要查多层文件，靠布隆过滤器和缓存补救&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;磁盘碎片&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;页分裂会产生碎片&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;文件整体重写，天然紧凑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;典型代表&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;InnoDB、PostgreSQL&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;RocksDB、Cassandra、HBase&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话理解&lt;/strong&gt;：B-Tree 是&amp;quot;图书馆书架&amp;quot;，书按位置摆好，找书快、插书要挪位置；LSM 是&amp;quot;先记在便签上，攒一批再上架&amp;quot;，记便签飞快，但定期要整理书架，找书得翻几摞便签。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;2-wal所有存储引擎的保命符&#34;&gt;2. WAL：所有存储引擎的&amp;quot;保命符&amp;quot;&lt;/h3&gt;
&lt;p&gt;LSM 写 Memtable、B-Tree 改数据页，都是先动内存。断电怎么办？答案是 &lt;strong&gt;WAL（Write-Ahead Log，预写日志）&lt;/strong&gt;：&lt;strong&gt;任何修改先追加一条日志到磁盘，再改内存结构&lt;/strong&gt;。崩溃后重放日志恢复。&lt;/p&gt;
&lt;p&gt;这个思想无处不在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;InnoDB 的 redo log 是 WAL；&lt;/li&gt;
&lt;li&gt;PostgreSQL 的 WAL 直接就叫 WAL；&lt;/li&gt;
&lt;li&gt;Redis 的 AOF 本质也是 WAL；&lt;/li&gt;
&lt;li&gt;Kafka 的消息日志、ZooKeeper 的事务日志，全是同一个套路。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话理解&lt;/strong&gt;：WAL 就是把&amp;quot;随机写&amp;quot;变成&amp;quot;顺序写&amp;quot;——磁盘顺序追加比随机写快几个数量级，顺便白捡了崩溃恢复能力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;3-行存-vs-列存oltp-与-olap-的分水岭&#34;&gt;3. 行存 vs 列存：OLTP 与 OLAP 的分水岭&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行存&lt;/strong&gt;：一行的所有列连续存放。适合&amp;quot;查一个人的全部信息&amp;quot;（点查、事务），即 OLTP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;列存&lt;/strong&gt;：一列的所有值连续存放。适合&amp;quot;统计一百万人的平均年龄&amp;quot;（聚合、分析），即 OLAP。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么列存分析快？小数字例子：一张 100 列的表，分析查询只用 5 列。行存要把 100 列全部从磁盘读出来再丢弃 95 列；列存只读那 5 列，IO 直接省 95%。而且单列数据类型相同，压缩率极高（比如&amp;quot;性别&amp;quot;列去重后就两个值，位图编码一下几乎不占空间）。&lt;/p&gt;
&lt;p&gt;代表：Parquet、ORC、ClickHouse、DuckDB 是列存；MySQL、PostgreSQL 是行存。Snowflake、BigQuery 这类数仓全部站在列存一边。&lt;/p&gt;
&lt;h3 id=&#34;4-编码与演进-schema-变了老数据怎么办&#34;&gt;4. 编码与演进： schema 变了，老数据怎么办&lt;/h3&gt;
&lt;p&gt;存量还有个隐藏维度：&lt;strong&gt;数据格式会随时间演化&lt;/strong&gt;。JSON 灵活但没约束；Protocol Buffers / Thrift / Avro 这类二进制编码通过&lt;strong&gt;字段编号&lt;/strong&gt;实现前后兼容——新增字段给新编号，删掉字段不动旧编号，新老代码就能和平共处。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 1&lt;/strong&gt;：滚动升级时&amp;quot;新代码读老数据&amp;quot;和&amp;quot;老代码读新数据&amp;quot;会同时发生。所以兼容性必须是&lt;strong&gt;双向&lt;/strong&gt;的：向后兼容（新读老）+ 向前兼容（老读新）。只考虑一个方向，升级到一半必出事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二部分-流量数据怎么动&#34;&gt;第二部分 流量：数据怎么动&lt;/h2&gt;
&lt;p&gt;单台机器存不下、扛不住、会宕机——这是&amp;quot;流量篇&amp;quot;要解决的三个问题。对应三板斧：&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;5-复制主从多主无主三种政治体制&#34;&gt;5. 复制：主从、多主、无主，三种政治体制&lt;/h3&gt;
&lt;p&gt;复制的目标是可用性和读扩展，但引入了全书最核心的矛盾：&lt;strong&gt;副本之间怎么保持一致&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主从复制（单主）&lt;/strong&gt;：所有写请求走 Leader，Leader 把变更日志同步给 Follower。读可以分摊到 Follower。简单、直觉，MySQL、PostgreSQL、MongoDB（副本集）、Kafka 分区副本都是这个思路。&lt;/p&gt;
&lt;p&gt;但&amp;quot;Follower 的数据是旧版本&amp;quot;这件事，会带来一连串用户可感知的诡异现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;你刚发的朋友圈自己刷新后消失了&lt;/strong&gt; → 需要&lt;strong&gt;读己之写（read-your-writes）&lt;/strong&gt;：用户写完之后，读请求必须走 Leader 或带版本号校验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;你的余额从 100 变成 80 又变回 100&lt;/strong&gt;（读到了更旧的副本）→ 需要&lt;strong&gt;单调读（monotonic reads）&lt;/strong&gt;：一个用户的连续读请求固定打到同一个副本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聊天里先看到回复再看到原帖&lt;/strong&gt;（分区的数据流向不同副本速度不同）→ 需要&lt;strong&gt;一致前缀读（consistent prefix reads）&lt;/strong&gt;：有因果关系的写入，任何读者都应该按相同顺序看到。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant C as 客户端
    participant L as Leader（主库）
    participant F1 as Follower 1（同步副本）
    participant F2 as Follower 2（异步副本）

    C-&amp;gt;&amp;gt;L: 写 x = 1
    L-&amp;gt;&amp;gt;L: 写本地 WAL 并提交
    L-&amp;gt;&amp;gt;F1: 同步复制：等 F1 确认
    F1--&amp;gt;&amp;gt;L: ACK
    L--&amp;gt;&amp;gt;C: 写入成功
    L-&amp;gt;&amp;gt;F2: 异步复制：后台慢慢发
    Note over F2: 此时 F2 还是旧值 x = 0&amp;lt;br/&amp;gt;复制延迟窗口
    C-&amp;gt;&amp;gt;F2: 读 x
    F2--&amp;gt;&amp;gt;C: x = 0（读到旧数据！）
    Note over C,F2: 这就是&amp;#34;读己之写&amp;#34;被破坏的瞬间&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;同步 vs 异步是个硬权衡&lt;/strong&gt;：全同步，任何一个副本宕机写入就卡死；全异步，Leader 宕机时没同步完的写入直接丢失。实践中的折中是&amp;quot;半同步&amp;quot;——一个副本同步、其余异步（MySQL 的 semi-sync 就是这个）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多主复制&lt;/strong&gt;：多个节点都能接受写，彼此同步。适合多数据中心就近写入，但&lt;strong&gt;写冲突&lt;/strong&gt;是噩梦——两个机房同时改同一条数据，谁说了算？常见解法：LWW（最后写入者胜，靠时间戳，会丢数据）、CRDT（无冲突数据结构）、应用层解决。CouchDB 走这条路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;无主复制（Dynamo 风格）&lt;/strong&gt;：没有 Leader，写入时客户端把数据发给 N 个副本中的任意 W 个，读的时候从 R 个里读，只要 &lt;strong&gt;W + R &amp;gt; N&lt;/strong&gt; 就能保证读到最新值（法定人数，quorum）。经典参数 N=3, W=2, R=2。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;经典案例：Amazon Dynamo 论文与购物车&lt;/strong&gt;。2007 年 Amazon 发表 Dynamo 论文，动机极其务实：&lt;strong&gt;购物车绝不能拒绝写入&lt;/strong&gt;——用户加购物车被拒，就是真金白银的损失。所以 Dynamo 选择永远可写（AP），哪怕网络分区时也接受写入，事后再合并冲突版本（著名的&amp;quot;向量时钟&amp;quot;就出自这篇论文）。这篇论文直接催生了 Riak、Cassandra、Voldemort 一整代 NoSQL。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话理解&lt;/strong&gt;：单主是&amp;quot;独裁&amp;quot;，多主是&amp;quot;联邦&amp;quot;，无主是&amp;quot;全民投票&amp;quot;。越民主写入越自由，但事后摆平分歧的成本越高。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;6-分区数据切蛋糕的两种刀法&#34;&gt;6. 分区：数据切蛋糕的两种刀法&lt;/h3&gt;
&lt;p&gt;单机存不下就分区（分片）。两种切法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;按 key 范围分区&lt;/strong&gt;：字典式切分，A&lt;del&gt;M 一个分区、N&lt;/del&gt;Z 一个分区。范围查询（&amp;ldquo;查 7 月的所有订单&amp;rdquo;）天然高效，但&lt;strong&gt;热点&lt;/strong&gt;问题明显——如果 key 是时间戳，所有新写入全砸在最后一个分区上。Bigtable、HBase 用这套。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;按键的哈希分区&lt;/strong&gt;：key 过一遍哈希函数打散。负载均匀，但范围查询要 scatter-gather 到所有分区，效率骤降。Cassandra、DynamoDB 用这套（Cassandra 实际上是&amp;quot;哈希定分区 + 分区内按 key 排序&amp;quot;的混合体）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再平衡（rebalancing）&lt;/strong&gt; 是分区系统真正的试金石：加机器、减机器、某分区过热，数据都要搬家。要求搬家期间读写不停、搬迁量最小。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 扩容前[&amp;#34;扩容前：3 节点，每节点 4 个虚拟分区&amp;#34;]
        N1[&amp;#34;节点 A&amp;lt;br/&amp;gt;v0 v1 v2 v3&amp;#34;]
        N2[&amp;#34;节点 B&amp;lt;br/&amp;gt;v4 v5 v6 v7&amp;#34;]
        N3[&amp;#34;节点 C&amp;lt;br/&amp;gt;v8 v9 v10 v11&amp;#34;]
    end
    subgraph 扩容后[&amp;#34;扩容后：新节点 D 加入&amp;lt;br/&amp;gt;只搬走 3 个虚拟分区，其余不动&amp;#34;]
        N1b[&amp;#34;节点 A&amp;lt;br/&amp;gt;v0 v1 v2&amp;#34;]
        N2b[&amp;#34;节点 B&amp;lt;br/&amp;gt;v4 v5 v6&amp;#34;]
        N3b[&amp;#34;节点 C&amp;lt;br/&amp;gt;v8 v9 v10&amp;#34;]
        N4[&amp;#34;节点 D&amp;lt;br/&amp;gt;v3 v7 v11&amp;lt;br/&amp;gt;（从 A/B/C 各搬一个）&amp;#34;]
    end
    扩容前 --&amp;gt;|&amp;#34;固定数量虚拟分区策略&amp;lt;br/&amp;gt;只迁移少量 vNode&amp;#34;| 扩容后&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;主流策略就是图里这个：&lt;strong&gt;固定数量的虚拟分区&lt;/strong&gt;（vNode）——分区数远多于节点数，加节点时从每个老节点&amp;quot;匀&amp;quot;几个虚拟分区过来。Riak、Cassandra 都这么干。反例是取模哈希（hash mod N）：节点数一变，几乎全部 key 都要搬家，这就是一致性哈希要解决的原始问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 2&lt;/strong&gt;：分区键一旦选定，几乎不可更改。用 user_id 做分区键的系统，后来想做&amp;quot;按时间范围全局扫描&amp;quot;，会发现每个查询都要打满全部分区。选分区键时想清楚：&lt;strong&gt;你的主要查询模式是什么，热点在哪&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;7-派生数据一个数据系统的输出是另一个系统的输入&#34;&gt;7. 派生数据：一个数据系统的输出，是另一个系统的输入&lt;/h3&gt;
&lt;p&gt;Kleppmann 有个非常锋利的视角：&lt;strong&gt;缓存、搜索索引、数据仓库，本质上都是派生数据&lt;/strong&gt;——它们都是从某个&amp;quot;真相源&amp;quot;（source of truth）加工出来的冗余副本。&lt;/p&gt;
&lt;p&gt;一旦接受这个设定，很多架构问题豁然开朗：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存和数据库不一致？因为派生数据没有和源数据同步更新。&lt;/li&gt;
&lt;li&gt;ES 里的搜索结果和 MySQL 对不上？同理。&lt;/li&gt;
&lt;li&gt;数仓报表延迟一天？因为派生管道是 T+1 的批处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是问题从&amp;quot;怎么让缓存不失效&amp;quot;变成&amp;quot;&lt;strong&gt;派生管道怎么设计&lt;/strong&gt;&amp;quot;，这正是流处理要回答的，下文第 10 节展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三部分-回路系统怎么自我纠错&#34;&gt;第三部分 回路：系统怎么自我纠错&lt;/h2&gt;
&lt;p&gt;存量和流量解决了&amp;quot;存得下、流得动&amp;quot;，剩下的问题是：&lt;strong&gt;当多个客户端并发操作、机器会宕机、网络会分区时，系统凭什么保证数据不出错？&lt;/strong&gt; 这就是回路——一致性保障与反馈机制。&lt;/p&gt;
&lt;h3 id=&#34;8-事务与隔离级别用弱隔离换性能代价是异常&#34;&gt;8. 事务与隔离级别：用&amp;quot;弱隔离&amp;quot;换性能，代价是异常&lt;/h3&gt;
&lt;p&gt;事务（ACID）是数据库给程序员的最大善意：&lt;strong&gt;把一堆操作打包成原子单元，要么全成要么全不成，并发的麻烦我来扛&lt;/strong&gt;。但&amp;quot;我来扛&amp;quot;是有价格的——严格串行执行性能太差，于是数据库发明了隔离级别这个&amp;quot;滑杆&amp;quot;，让你自己选扛多少：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;隔离级别&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;防住的异常&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;防不住的异常&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;谁在用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;读未提交&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;几乎啥都不防&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;脏读、一切&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;基本没人用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;读提交（RC）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;脏读、脏写&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;不可重复读、写倾斜&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Oracle、PostgreSQL 的默认&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;快照隔离（SI）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;上面所有 + 不可重复读&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;写倾斜&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;PostgreSQL 的 RR、MySQL InnoDB 的 RR&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;串行化&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一切&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;无&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;性能最好时的梦想&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;快照隔离（Snapshot Isolation）&lt;/strong&gt; 值得单独说，因为它是现代数据库的主力。原理是 MVCC（多版本并发控制）：每次写入不覆盖旧值，而是生成新版本，每个事务看到&amp;quot;它开始那一刻&amp;quot;的数据快照。读不阻塞写、写不阻塞读，性能极佳。InnoDB 的 undo log、PostgreSQL 的多版本堆表，都是 MVCC 实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写倾斜（write skew）是快照隔离最大的盲区&lt;/strong&gt;，也是面试和实战最容易踩的坑。小例子：医院排班系统要求&amp;quot;至少有一名医生值班&amp;quot;，当前有 Alice 和 Bob 两人值班。两人同时点&amp;quot;请假&amp;quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alice 的事务读到&amp;quot;2 人值班&amp;quot;，判断&amp;quot;我请假后还剩 1 人，OK&amp;quot;，提交；&lt;/li&gt;
&lt;li&gt;Bob 的事务同样读到&amp;quot;2 人值班&amp;quot;（快照！他看不到 Alice 未提交的修改），同样判断 OK，提交；&lt;/li&gt;
&lt;li&gt;结果：两人都不值班了，约束被破坏。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意这不是脏读也不是不可重复读——每个事务内部看到的数据完全一致，但&lt;strong&gt;两个事务基于同一份过期前提做出了互相冲突的决定&lt;/strong&gt;。解法只有：升级到串行化，或者用 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; 物化冲突（把隐含的读前提变成显式行锁）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑指南 3&lt;/strong&gt;：凡是有&amp;quot;读取一个约束条件，然后据此决定写入&amp;quot;模式的代码（库存扣减、名额抢占、余额校验），在 RR/快照隔离下都可能写倾斜。别指望 ORM 救你，老老实实 &lt;code&gt;FOR UPDATE&lt;/code&gt; 或上串行化。&lt;/p&gt;
&lt;/blockquote&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;：单线程跑事务（Redis 单线程模型、VoltDB），简单暴力，内存够快时性能惊人；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两阶段锁（2PL）&lt;/strong&gt;：读写都加锁，性能差但正确，MySQL 的串行化级别就是这么实现的，也是死锁的主要来源；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可串行化快照隔离（SSI）&lt;/strong&gt;：快照隔离之上检测危险读写依赖，有冲突就中止重试。PostgreSQL 9.1+ 的串行化级别用的就是它，是近几十年事务领域最重要的学术成果落地。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;9-共识与线性一致性分布式系统的终极回路&#34;&gt;9. 共识与线性一致性：分布式系统的&amp;quot;终极回路&amp;quot;&lt;/h3&gt;
&lt;p&gt;事务解决的是&amp;quot;单机/单系统内的并发正确性&amp;quot;，共识（consensus）解决的是更难的问题：&lt;strong&gt;一群会宕机、消息会延迟的机器，怎么对&amp;quot;某件事&amp;quot;达成一致&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;先纠正一个常见混淆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线性一致性（Linearizability）&lt;/strong&gt;：让系统表现得&amp;quot;像只有一份数据&amp;quot;——任何人读到 x=1 之后，不可能再读到 x=0。它是对&lt;strong&gt;单个对象&lt;/strong&gt;的读写一致性承诺。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共识（Consensus）&lt;/strong&gt;：多个节点对&lt;strong&gt;某个值/某条决策&lt;/strong&gt;达成不可逆的一致，典型算法 Paxos、Raft、Zab。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者的关系：要实现线性一致的存储，背后几乎必然要靠共识算法选主或定序。ZooKeeper（Zab）、etcd（Raft）就是共识系统的工业代表，整个 Kubernetes 的正确性底座就是 etcd 里的那套 Raft。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;经典案例：Google Spanner 与 TrueTime&lt;/strong&gt;。Spanner 要在全球范围提供&lt;strong&gt;外部一致性&lt;/strong&gt;（比线性一致更强，跨事务的全局顺序），最大的拦路虎是：分布式系统里各机器时钟不可靠，你没法靠时间戳判断两个事务谁先谁后。Google 的解法粗暴而硬核——&lt;strong&gt;给数据中心装 GPS 接收机和原子钟&lt;/strong&gt;，把时钟误差压缩到一个已知的小区间（TrueTime API 返回的不是一个时间点，而是&amp;quot;真实时间必然落在的这个区间&amp;quot;）。提交事务时，如果发现误差区间还没过去，就&lt;strong&gt;故意等待&lt;/strong&gt;（commit wait）直到确定。用硬件换确定性，用等待换正确性——这是&amp;quot;回路强度可以花钱买&amp;quot;的最著名示范。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;FLP 不可能定理&lt;/strong&gt;泼一盆冷水：在纯异步网络里，哪怕只有一台机器可能崩溃，也不存在&lt;strong&gt;确定性&lt;/strong&gt;的、保证能终止的共识算法。工程上的出路是引入&amp;quot;部分同步&amp;quot;假设（网络总会恢复）和超时机制——所以 Raft/Paxos 的正确性不依赖时钟，但&lt;strong&gt;活性&lt;/strong&gt;（liveness，能不能往前走）依赖。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;极端案例：比特币与拜占庭容错&lt;/strong&gt;。上面说的共识算法都假设节点&amp;quot;可能宕机但不会撒谎&amp;quot;（崩溃容错 CFT）。比特币面对的是更恶劣的环境：&lt;strong&gt;节点可能作恶&lt;/strong&gt;（拜占庭将军问题）。中本聪的解法是放弃确定性共识，改用工作量证明（PoW）+ 最长链规则做&lt;strong&gt;概率性共识&lt;/strong&gt;——一笔交易等 6 个区块确认，被推翻的概率指数级下降但永不为零。性能极差（比特币全网约 7 TPS，对比 Visa 峰值数万 TPS），换来的是&amp;quot;无许可环境下没人能篡改历史&amp;quot;。这是把回路强度推到极限、把流量吞吐牺牲殆尽的极端案例，也完美印证了我们的框架：&lt;strong&gt;一切都是权衡&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;共识落地前，还有个更现实的问题：故障检测&lt;/strong&gt;。分布式系统判断&amp;quot;某节点死了&amp;quot;的唯一办法是超时——但超时无法区分&amp;quot;节点真死了&amp;quot;和&amp;quot;节点只是 GC 停顿了 30 秒&amp;quot;（Java 应用的 Full GC、虚拟机的 steal time、网络拥塞都会造成这种&amp;quot;假死&amp;quot;）。于是就有了&lt;strong&gt;脑裂（split-brain）&lt;/strong&gt;：老 Leader 以为自己还是 Leader，新 Leader 已经被选出来，两个&amp;quot;主&amp;quot;同时接受写入，数据互相覆盖。&lt;/p&gt;
&lt;p&gt;工程解法是 &lt;strong&gt;fencing token（防护令牌）&lt;/strong&gt;：每次选主，共识系统发一个单调递增的编号，存储层只接受&amp;quot;编号比上次大&amp;quot;的写入。老 Leader 拿着旧编号来写，直接被拒。这个小机制在 ZooKeeper（zxid）、etcd（revision）里都有对应物。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;业界掌故&lt;/strong&gt;：Kleppmann 本人就是分布式锁话题的&amp;quot;圈内名人&amp;quot;——2016 年他发文《How to do distributed locking》批判 Redis 的 Redlock 算法在时钟跳变和进程暂停下不安全，引来 Redis 作者 antirez 的公开反驳，两人来回论战了好几轮，成为分布式领域最著名的公开辩论之一。争论的核心正是上面这些：时钟不可信、暂停不可测，所以任何分布式锁要么上 fencing token，要么干脆用共识系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;10-批处理-vs-流处理同一枚硬币的两面&#34;&gt;10. 批处理 vs 流处理：同一枚硬币的两面&lt;/h3&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;——一堆文件摆在那，MapReduce/Spark 跑一遍，输出结果。优点是吞吐高、可重跑、容错简单（任务挂了重跑）；缺点是延迟以小时计。Hadoop/MapReduce 的黄金时代（Google 2004 年论文、Yahoo 孵化 Hadoop）就是这套世界观。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流处理&lt;/strong&gt;：数据是&lt;strong&gt;无界的&lt;/strong&gt;——事件源源不断地来，Kafka + Flink/Storm 逐条处理，延迟毫秒级。难点：乱序、恰好一次语义（exactly-once）、容错状态恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Lambda 架构&lt;/strong&gt;：流处理快但不准（可能丢、可能重复），批处理准但慢，那就两条都跑——流处理给实时近似结果，批处理定期用精确结果覆盖。维护两套系统、两份代码，运维成本翻倍，是出名的&amp;quot;说起来美、做起来痛&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kappa 架构&lt;/strong&gt;：LinkedIn 的 Jay Kreps（Kafka 作者）提出的简化——&lt;strong&gt;只要流处理一条腿&lt;/strong&gt;。历史数据重算？把 Kafka 的日志保留期拉长，让流处理程序从头重放一遍就行。这个架构能成立的前提，是 Kafka 把&amp;quot;消息队列&amp;quot;做成了&amp;quot;可重放的日志存储&amp;quot;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 数据源
        DB[(&amp;#34;业务数据库&amp;lt;br/&amp;gt;MySQL&amp;#34;)]
        APP[&amp;#34;应用事件&amp;#34;]
    end
    DB --&amp;gt;|&amp;#34;CDC 变更数据捕获&amp;lt;br/&amp;gt;（Debezium 监听 binlog）&amp;#34;| K[&amp;#34;Kafka&amp;lt;br/&amp;gt;（可重放的日志存储）&amp;#34;]
    APP --&amp;gt; K
    K --&amp;gt; S[&amp;#34;流处理引擎&amp;lt;br/&amp;gt;Flink / Kafka Streams&amp;#34;]
    S --&amp;gt; V1[&amp;#34;派生视图：缓存 Redis&amp;#34;]
    S --&amp;gt; V2[&amp;#34;派生视图：搜索索引 ES&amp;#34;]
    S --&amp;gt; V3[&amp;#34;派生视图：数仓 ClickHouse&amp;#34;]
    K --&amp;gt;|&amp;#34;日志长期保留&amp;lt;br/&amp;gt;需要重算时从头重放&amp;#34;| S
    subgraph KP[&amp;#34;Kappa / 批流一体&amp;#34;]
        S
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这张图里藏着 DDIA 全书最重要的思想之一：&lt;strong&gt;数据库和消息队列正在融合&lt;/strong&gt;。Kafka 是可以当数据库用的日志（有保留期、可重放、有流处理）；反过来，数据库的 binlog/WAL 通过 CDC（Change Data Capture）技术流出来，就变成了消息流。Debezium 监听 MySQL binlog 把每一次行变更变成 Kafka 消息，是当下&amp;quot;派生数据管道&amp;quot;的标准做法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;经典案例：Kafka 从 LinkedIn 内部管道到行业标准&lt;/strong&gt;。Kafka 诞生时（2010 年前后）只是 LinkedIn 内部的一个活动流（activity stream）和日志管道，目标是扛住&amp;quot;每个用户的每个页面浏览都要记录&amp;quot;的巨大流量。它的关键设计——把消息组织成&lt;strong&gt;分区有序日志&lt;/strong&gt;、消费者自己维护 offset、依赖磁盘顺序写+page cache 而非内存——让它意外地发现：这个东西不只是消息队列，而是&amp;quot;以日志为中心的存储系统&amp;quot;。Confluent 成立后，Kafka 成了流处理事实标准，Kreps 那篇《The Log: What every software engineer should know about real-time data&amp;rsquo;s unifying abstraction》（2013）把&amp;quot;日志是数据系统统一抽象&amp;quot;的思想讲透了——和 DDIA 第 3 章&amp;quot;日志结构存储&amp;quot;一脉相承。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;11-全书串讲用三个词复盘四个案例&#34;&gt;11. 全书串讲：用三个词复盘四个案例&lt;/h2&gt;
&lt;p&gt;现在用&amp;quot;存量、流量、回路&amp;quot;的框架，把本文案例快速过一遍，你会发现它们只是同一个框架的不同参数组合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kafka&lt;/strong&gt;：存量用顺序日志（LSM 的近亲），流量靠分区+副本，回路选择&amp;quot;至少一次/恰好一次可配&amp;quot;的务实路线。→ 流量优先，回路可调。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamo&lt;/strong&gt;：无主复制+永远可写，回路是最终一致+应用层冲突解决。→ 流量（可用性）压倒一切，回路最弱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spanner&lt;/strong&gt;：全球分区+同步复制，回路用 TrueTime 堆到最强（外部一致性）。→ 回路优先，花钱买。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;比特币&lt;/strong&gt;：回路要求拜占庭容错（最强敌对环境），代价是吞吐 7 TPS。→ 回路极限，流量牺牲殆尽。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;没有银弹，只有取舍。&lt;/strong&gt; 这就是 Kleppmann 反复想说、而无数&amp;quot;中间件选型指南&amp;quot;不会告诉你的事。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;12-带走的清单&#34;&gt;12. 带走的清单&lt;/h2&gt;
&lt;p&gt;读完 DDIA，做系统设计时问自己这几句话：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;存量三问&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写入多还是读取多？写多读少看 LSM（RocksDB/Cassandra），读多写少看 B-Tree（MySQL），分析查询上列存（ClickHouse/Parquet）。&lt;/li&gt;
&lt;li&gt;崩溃恢复靠什么？找到那个 WAL——它一定存在，可能只是叫别的名字（redo log / AOF / binlog / journal）。&lt;/li&gt;
&lt;li&gt;schema 演进支持双向兼容吗？向前向后都要，滚动升级时缺一不可。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;流量三问&lt;/strong&gt;&lt;/p&gt;
&lt;ol start=&#34;4&#34;&gt;
&lt;li&gt;复制延迟用户能感知吗？能，就按需要补读己之写、单调读、一致前缀读，别等用户来报 bug。&lt;/li&gt;
&lt;li&gt;分区键选对了吗？对照主要查询模式和热点，选错分区键≈架构重来。&lt;/li&gt;
&lt;li&gt;缓存/索引/数仓是不是派生数据？是，就把&amp;quot;同步管道&amp;quot;当成一等公民来设计（CDC + 流处理），而不是事后打补丁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;回路三问&lt;/strong&gt;&lt;/p&gt;
&lt;ol start=&#34;7&#34;&gt;
&lt;li&gt;业务能容忍什么级别的一致性？能用最终一致就别上线性一致，省下的都是真金白银。&lt;/li&gt;
&lt;li&gt;有没有&amp;quot;读约束再写入&amp;quot;的代码？有，检查写倾斜——&lt;code&gt;FOR UPDATE&lt;/code&gt; 或串行化，二选一。&lt;/li&gt;
&lt;li&gt;选主/分布式锁靠什么？别自己造轮子，ZooKeeper/etcd 这类共识系统才是正解（Redis 分布式锁的争议——Martin Kleppmann 本人就是那篇著名批判文章《How to do distributed locking》的作者，还设计了&amp;quot;马丁小花&amp;quot;fencing token 方案）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;最后的避坑清单&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;别把默认隔离级别当成&amp;quot;安全级别&amp;quot;——PostgreSQL/MySQL 的默认都不是串行化，写倾斜在看着你。&lt;/li&gt;
&lt;li&gt;别迷信&amp;quot;分布式 = 高可用&amp;quot;——复制和分区引入的故障模式（脑裂、时钟漂移、复制滞后）比单机复杂一个数量级，Jepsen 测试报告（Kyle Kingsbury 对各大数据库的&amp;quot;虐测&amp;quot;）里全是血泪。&lt;/li&gt;
&lt;li&gt;别在不了解权衡时引用 CAP——CAP 只说&amp;quot;网络分区时一致性和可用性二选一&amp;quot;，而网络不分区时你还有延迟与一致性的权衡（PACELC 才是完整版）。&lt;/li&gt;
&lt;li&gt;时间不可信：进程会 GC 暂停、线程会被调度、时钟会跳变。所有依赖&amp;quot;本地时钟先后关系&amp;quot;的分布式逻辑都值得怀疑——这正是 Spanner 要装原子钟的原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;13-结语野猪书为什么是神书&#34;&gt;13. 结语：野猪书为什么是神书&lt;/h2&gt;
&lt;p&gt;市面上讲分布式的书，要么讲理论（一堆证明，读完不会写代码），要么讲产品（一堆配置，换个组件就过时）。DDIA 恰好卡在中间：&lt;strong&gt;它讲的是&amp;quot;不会过时的权衡&amp;quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;LSM 和 B-Tree 的取舍，二十年前成立，二十年后还会成立；复制滞后引发的那些&amp;quot;诡异现象&amp;quot;，今天在 MySQL 从库上出现，明天就会在你的新数据库上换个马甲出现。工具会死，权衡永存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顺带给个阅读路径建议&lt;/strong&gt;：这本书 12 章不必按顺序啃——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日常写业务代码的：先读第 3 章（存储引擎）、第 7 章（事务），性价比最高；&lt;/li&gt;
&lt;li&gt;做中间件/基础架构的：第 5、6 章（复制、分区）和第 9 章（共识）是主菜；&lt;/li&gt;
&lt;li&gt;做数据平台/实时数仓的：第 10、11 章（批处理、流处理）直接对口；&lt;/li&gt;
&lt;li&gt;第 8 章（分布式系统的麻烦）建议所有人都读，它是全书&amp;quot;劝退幻想&amp;quot;的一章，读完你会对时钟和网络产生健康的怀疑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用一句话收尾，也是这本书给我最大的思维升级：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;不要问&amp;quot;这个系统支持什么特性&amp;quot;，要问&amp;quot;这个系统在存量、流量、回路上各做了什么取舍，这些取舍和我的业务匹配吗&amp;quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;能问出这个问题，这本书就没白读。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
