这一讲难在它不是一条线,是四条线并排放着。
选型(三类数据库)· 事务(乐观锁)· 主从(复制)· 分布式(切分)——
它们解决的是四个不相干的问题,原文按顺序讲下来,很容易读成"数据库的四个特性"。先把这张表刻进去,后面就不会串:
| 这一节 | 解决什么问题 | 它的反面是什么 |
|---|---|---|
| 一 · 三类数据库 | 怎么用——使用界面之争 | 不是能力之争,三者能干的事高度重叠 |
| 二三 · 事务 | 并发和崩溃会不会弄脏数据 | 不解决容量、不解决机器坏 |
| 四 · 主从 | 机器会坏(可用性 + 持久性) | 每台存全量,不解决放不下 |
| 五六 · 分布式 | 放不下 / 扛不住(容量 + 吞吐) | 每台存一部分,不解决机器坏 |
主从和分片是垂直的两件事,真实系统两个都要。 这是全篇最容易混的一点,第五节会画图。
这一讲的演示脚本(都可以直接 python3 跑):
| 脚本 | 演什么 | 配合哪节 |
|---|---|---|
代码/乐观锁.py |
版本链 · 撞车回滚 · 幻读(为什么必须记读条件) | 三 |
代码/断网那一刻.py |
拔网线后 CP 与 AP 各自发生什么 · 过半数与脑裂 | 四 · 六 |
代码/一个字典拆成三台.py |
分片到底是什么——一个大 dict 拆成三个 + 一个函数 | 五(先跑这个) |
代码/分片.py |
三种分片函数,扩容各要搬多少数据(真实数字) | 五(再跑这个) |
一 · 三类数据库:同一个东西的三种「使用界面」
原文按"关系型 / 文档型 / KV"列了三类。它们的差别不在能干什么,在怎么用——这正是 32 讲那个"使用界面"的话题。
1.1 强 schema vs 无 schema,就是 dataclass vs dict
# 关系型:强 schema —— 写错了当场报错
@dataclass
class 用户:
名字: str
年龄: int
用户(名字="小王", 年龄="二十") # 你至少能靠类型检查揪出来
# 文档型:无 schema —— 写错了照收不误
{"名字": "小王", "年龄": "二十"} # 存进去了,三个月后才发现
{"姓名": "小李", "岁数": 20} # 字段名都改了,也存进去了
原文那句"质量保障体系弱化,数据可能被弄脏而不自知",说的就是第二段。
好处也是真的:加字段不用改表结构。这就是它的全部取舍。
1.2 「KV 是数据库的基础」是什么意思
原文这句最容易滑过去:
“从实现角度来说,键值存储是数据库的基础。每一组数据库的索引,往往背后就是一组键值存储。”
拆开就是——一张有两个索引的表,物理上是三个 KV:
# users 表:主键 id,另外在 邮箱 和 年龄 上建了索引
主键KV = { 1001: {"名":"小王","邮箱":"w@x.com","岁":20}, # ← 数据本体在这
1002: {"名":"小李","邮箱":"l@x.com","岁":31} }
邮箱KV = { "w@x.com": 1001, # ← 索引 = 另一个 KV
"l@x.com": 1002 }
年龄KV = { (20, 1001): None, # ← 有序 KV,才能范围扫
(31, 1002): None }
SELECT * FROM users WHERE 邮箱='w@x.com' 实际发生的是:先查邮箱KV 拿到 1001,再查主键KV 拿整行。两次 KV 查找。
这一下就解释清楚了原文那两句看似矛盾的话:
- “使用上,KV 是数据库的特例”——数据库能建多个索引,KV 只有唯一索引(就是上面的"主键KV")
- “实现上,KV 是数据库的基础”——数据库的每个索引都是一个 KV,数据库 = 一组 KV + 一个查询规划器
二 · 事务:它到底在防什么
文字版的完整推导已经写在 笔记 · 为什么:事务这一节到底在说什么 里了,这里不重复。
这一节只留一张必须刻住的表,然后直接进脚本。
转账,A -= 50 和 B += 50 这两行之间有两种灾难:
| 灾难 | 长什么样 | 谁来防 |
|---|---|---|
| 崩溃 | 第一行跑完进程挂了 → 50 块凭空消失 | A 原子性(要么都算数要么都不算) D 持久性(说了成功就不许因断电而回退) |
| 并发 | 别人恰好在两行中间查账 → 总额对不上 | I 隔离性(中间状态不给别人看见) |
| —— | —— | C 一致性 = 不是机制,是结果:A+B 永远等于 800 |
ACID 不是并列的四件事。A/D 对付崩溃,I 对付并发,C 是前三者做到之后想要的那个结果。
原文四条并排列,最容易让人以为要分别实现四个东西。
而"一把大锁"(with 全局锁: 包住整个事务)确实能实现完整 ACID,问题是那个 with 里面含磁盘 IO、业务逻辑、甚至跨机器网络往返——这段时间全世界排队,吞吐掉成 1 ÷ 一个事务的耗时。
三 · ★ 乐观锁 = 把 git 那套搬进数据库
python3 代码/乐观锁.py
| 悲观锁(常规锁) | 乐观锁 | |
|---|---|---|
| 打比方 | 改文件前先把文件锁死,别人不许碰 | 各自在自己分支上改,提交时才检查撞没撞车 |
| 顺序 | 先互斥 → 再改 | 先改(改在草稿上)→ 提交时检查 → 没撞车才生效 |
| 代价 | 不管有没有冲突,都先付出等待 | 冲突罕见时几乎零成本;真撞车了整个事务白做、重来 |
| 数据库里的说法 | git 里的对应物 |
|---|---|
KEYi -> [(VER0,VAL0),(VER1,VAL1)] 版本链 |
一个文件的 commit 链 |
| 修改先进"草稿",别人读不到 | 在自己分支上 commit,还没 push |
| 提交时检查读条件 | push 前看远端有没有人动过我依赖的东西 |
| 撞车 → 整个事务回滚 | push 被拒 → 重来 |
3.1 脚本里的三个场景
场景一 · 改不同的行 → 两个事务同时跑,都成功,全程零互斥
场景二 · 抢同一行 → 后提交的那个【整个白做】,版本链里什么都没留下
场景三 · 幻读 → 同一段代码,只记 KEY 会放过去,记条件才拦得住
场景三是全篇最反直觉的一段,脚本把两种做法各跑了一遍:
做法:只记KEY
[T1] 读「岁>17」→ 命中 3 行 [1, 2, 3]
[T2] 改行4 = {'名': '小赵', '岁': 20} → 提交成功
[T1] 检查方式:只查我读过的行 [1, 2, 3]
✓ 没撞车 → 转正
✗ T1 提交成功了,但现在成年人其实有 4 个
做法:记条件
[T1] 检查方式:扫全库,查所有满足「岁>17」的行
✗ 撞车!行[4] 的已提交版本 > 我的基版本 v0
我读过的行一个都没变,但我读到的那个「集合」变了。 这个现象叫幻读(Phantom Read)。
所以原文说:要记的不是读过哪些name/age/address,而是"我读过所有 age>17 的条目"。
3.2 三个最容易读拧的点
| 原文那句 | 读拧成 | 其实是 |
|---|---|---|
| “提交的时候,锁住整个数据库” | 那不还是一把大锁? | 差别在锁多久,不在锁多大。大锁横跨整个事务(含 IO、网络);这里只锁住纯内存的版本号比对,微秒级。原文"把锁的粒度降到最低",降的是时间粒度 |
| “避免了出现死锁的可能” | 乐观锁有什么魔法 | 死锁的成因永远是"我拿着 A 等 B、你拿着 B 等 A",成环。乐观锁全程只有一把锁,提交那一瞬间拿、检查完就放,不存在持有一把锁再去等另一把——没有环可以成 |
| “必然有一个事务的提交会成功” | 我的事务不会被饿死 | 保证的是系统级不饥饿(总有人前进),不是个体级。热点行 + 慢事务,完全可能一次次撞车一次次重来。这就是乐观锁的适用边界:冲突罕见才划算 |
四 · 主从:复制,解决"机器会坏"
python3 代码/断网那一刻.py # 看第二部分
心智模型:主从 = 复印机。每台机器存的是全量数据,一模一样。
它跟容量、跟吞吐都没关系,它只回答一个问题:这台机器坏了怎么办。
4.1 为什么选主要「超过半数」
原文只说了"超过一半的节点同意",没说为什么。因为要防脑裂:
网络一断,两边各选一个主,各自接受写,数据从此分叉——两张 ok 回执,必有一张是假的。
"过半数"让这件事在数学上不可能:
5 个节点,过半 = 3 票,网络把它们劈成两半:
1 : 4 → 左边选不出,右边能选出主
2 : 3 → 左边选不出,右边能选出主
3 : 2 → 左边能选出主,右边选不出
4 : 1 → 左边能选出主,右边选不出
两个都"超过半数"的集合必然相交,而同一个节点不可能同时投给两个主。
脚本穷举了所有组合:反例 0 种。
4.2 为什么节点数要奇数
| 节点数 | 过半需要 | 最多能挂几台 | |
|---|---|---|---|
| 2 | 2 | 0 | 主挂了就选不出新主 |
| 3 | 2 | 1 | |
| 4 | 3 | 1 | ← 和 3 台一样,白加一台 |
| 5 | 3 | 2 | |
| 6 | 4 | 2 | ← 和 5 台一样,白加一台 |
4 台的容错能力和 3 台完全一样,你却多花了一台机器的钱、还多了一台会坏的机器。
偶数那台永远是白给的。
4.3 为什么"写要确保至少一个从收到了"
假设主写完就直接回 ok,然后主立刻宕机 → 所有从都没有这条数据 → 选谁当新主都是版本回退。
用户手里那张 ok 回执,又变成假的了。
这条约束、CAP 里选 C、36 讲的"绝不丢数据",说的是同一件事:承诺过的,必须活过任何单点故障。
五 · ★ 分布式:一个大 dict 装不下了
python3 代码/一个字典拆成三台.py # ← 先跑这个,只讲分片本身
python3 代码/分片.py # ← 再跑这个,讲函数怎么选
不要从"哈希分片 vs 范围分片"开始读这一节。 那是第 5.5 小节的事。
先把 5.1~5.4 这四步走完,分片这件事本身其实极简单。
5.1 一台机器的时候
36 讲的结论是:数据库 ≈ 一个能落盘的 dict。
用户表 = {"user01": "小王", "user02": "小李", ...}
用户表["user05"] # → 小刘
现在用户从 9 个变成 1 个亿。一台机器装不下了。
5.2 装不下就拆开 —— 这就是「分片」
机器 = [{}, {}, {}] # 三台,每台只存一部分
拆完只多出一个新问题:user05 该存进哪台?取的时候又去哪台找?
总不能三台挨个问(那还不如不拆)。所以需要一个函数:
def 该找哪台(key):
return int(key[-1]) % 3 # 拿末位数字对 3 取余
存的时候用它算,取的时候还用它算——同一个 key 过同一个函数,永远算出同一台,所以直奔那台就行。
0 号机: {'user03': '小张', 'user06': '小陈', 'user09': '小周'}
1 号机: {'user01': '小王', 'user04': '小赵', 'user07': '小杨'}
2 号机: {'user02': '小李', 'user05': '小刘', 'user08': '小黄'}
取 user05 → 函数算出 2 号机 → 去 2 号机拿到「小刘」
★ 分片的全部内容就这一句:
一个大 dict → N 个小 dict + 一个「该找哪台」的函数。
而原文那个"哈希分片 / 范围分片",只是这个函数的两种写法:
def 该找哪台(key): return hash(key) % 3 # 哈希分片:答案是【算】出来的
def 该找哪台(key): return 0 if key < "m" else 1 # 范围分片:答案是【查】出来的
它们不是两个新知识点,是同一个位置的两种填法。
5.3 加一台机器,为什么就要搬数据
三台不够,加到四台。函数必须跟着改:
return int(key[-1]) % 3 → return int(key[-1]) % 4
函数一改,很多 key 算出来的答案就变了。答案变了,那条数据就必须搬到新答案那台去,否则以后再也找不到它。
key 老家(%3) 新家(%4)
user01 1 1
user02 2 2
user03 0 3 ← 要搬
user04 1 0 ← 要搬
user05 2 1 ← 要搬
... 9 个里有 7 个要搬
★ 扩容要搬数据,不是因为"分布式很复杂",就是因为那个函数变了。
搬多少 = 有多少 key 的答案变了 = 完全取决于函数怎么写。
这就是原文那句"扩缩容会导致重新分片,重新分片意味着需要做数据的搬迁"的全部含义。
5.4 分片 ≠ 主从:它俩拆的是不同的东西
主从(第四节):三台存的是【同一个 dict 的三个复制品】
机器A = {全部 9 个}
机器B = {全部 9 个} 一模一样
机器C = {全部 9 个}
✓ A 坏了还有 B、C ✗ 每台还是得装下全部
分片(本节):三台存的是【同一个 dict 的三块】
0号机 = {3个} 1号机 = {3个} 2号机 = {3个}
✓ 装得下了 ✗ 坏一台呢?
脚本把 2 号机的硬盘"烧掉"了:
取 user02 → 2 号机 → ✗ 查无此人(数据没了)
取 user05 → 2 号机 → ✗ 查无此人(数据没了)
★ 分片一点也不解决可靠性。
这就是原文最容易读漏的那个词——“分片服务器一起构成一个单副本的数据库”:
三台加起来只是一份数据,不是三份。
所以真实系统是两个叠着用:
分片1 分片2 分片3 ← 竖着切:解决装不下 / 扛不住
│ │ │
┌────┼────┐ ┌────┼────┐ ┌────┼────┐
主 从 从 主 从 从 主 从 从 ← 横着复制:解决机器会坏
9 台机器,3 份数据。
5.5 函数怎么选:三种写法的三个后果
既然分片 = N 个小 dict + 一个函数,所有设计选择就都集中在这一个函数上。
写法一 · 哈希——答案是算出来的
def 该找哪台(key): return hash(key) % N
hash 把 key 彻底打散,分得很均匀 ✓。代价是相邻的 key 被打到了不同机器:
- 查
user004500 ~ user004600这种范围,得问遍所有机器再拼结果 ✗ - N 一变答案几乎全变 → 扩容搬 75% ✗
写法二 · 范围——答案是查出来的
def 该找哪台(key):
if key < "user004000": return 0
elif key < "user008000": return 1
else: return 2
相邻的 key 在同一台,范围查询只问一台 ✓。扩容就是在末尾插一条 if(把一片劈成两半),只搬那一片 ✓。
代价是:key 如果递增(时间戳、订单号、日志 ID),新数据永远落最后一片。分片.py 跑出来的:
今天新写入 1440 条: 机器0: 0 条 机器1: 0 条 机器2: 1440 条
另外两台在睡觉。 这叫热点。
写法三 · 一致性哈希(原文没讲,是工业界对写法一那个 75% 的答案)
思路一句话:别让答案依赖 N。把 key 和机器都映到同一个环上(0 ~ 2¹²⁸),key 顺时针遇到的第一台机器就是它的家。加一台机器 = 在环上插一个点,只有它到前一个点之间那段的 key 换家 → 搬 1/N。
| 哈希 | 范围 | 一致性哈希 | |
|---|---|---|---|
| 分得均匀 | ✓ | ✗ 看 key 长什么样 | ✓ 靠虚拟节点 |
| 范围查询 | ✗ 问遍所有台 | ✓ 只问一台 | ✗ 问遍所有台 |
| 递增 key 会热点 | ✓ 不会 | ✗ 全压最后一片 | ✓ 不会 |
| 3→4 台要搬(12000 个 key 实测) | 75.2% | 16.7% | 23.4% |
| 谁在用 | MySQL 分库分表 | HBase、TiDB | Redis Cluster、Cassandra |
没有"最好的分片函数",只有"你的查询长什么样"。
要按时间区间翻账单(对账、日志检索)就得范围分片然后忍热点;
只按 ID 点查、写入量大又怕热点(用户表、会话)就用哈希然后忍搬迁。
5.6 搬家本身怎么搬
5.3 说"要搬 75%",但搬的过程才是硬骨头——因为不能停机搬。
设想正在把 user05 从 2 号机搬到 1 号机,就在这一刻:
| 来了个… | 麻烦在哪 |
|---|---|
读 user05 |
该问 2 号机(还没搬完)还是 1 号机(已经搬了)? |
写 user05 |
写 2 号机?它马上要被删。写 1 号机?可读还在走 2 号机 |
| 进程挂了 | 两边各有一半,重启后怎么知道搬到哪了 |
原文那句"有一部分数据在源节点,一部分数据在目标节点",说的就是这个。工程上的标准解法是双写 + 灰度切读:
阶段1 读走源 · 写同时落源和目标 ← 目标在后台追数据
阶段2 数据追平 → 读切到目标 · 写仍双落 ← 目标出问题能立刻切回来
阶段3 确认没事 → 停掉源
这跟 36 讲上线新存储时的"双写"是同一个手法:不敢一步切换,就让新旧并行一段,
始终留一条能撤回的路。
六 · ★ CAP:把「机器之间总能说上话」这个假设拿掉
python3 代码/断网那一刻.py # 看第一部分
前面五节一直有个隐含假设:机器之间的网络是通的。CAP 就是把这个假设拿掉。
6.1 先定位 CAP 在哪一层
这是最关键的一句,先看图:
分片1 分片2 分片3 ← 横排:存的是【不同的】数据
│ │ │
┌────┼────┐ ┌────┼────┐ ┌────┼────┐
主 从 从 主 从 从 主 从 从
└─ CAP 说的是这一竖列 ─┘ ← 竖列:存的是【同一份】数据的拷贝
- 横排的分片之间根本不需要一致——它们各管各的一块,本来就不一样
- 竖列的主从之间才需要一致——它们是同一份数据的复制品,必须一样
所以 CAP 是第四节(主从)那一层的问题,不是第五节(分片)的。
分片只是让它更常见:机器越多,网线越容易断。
6.2 断网那一刻的两种结局
CP —— 保一致,牺牲可用
✂ 拔网线
往【甲】写 200 → ✗ 拒绝:我联系不上另一半,不敢答应你
读甲 → 100 读乙 → 100 两边一致,但谁都写不进去
AP —— 保可用,牺牲一致
✂ 拔网线
往【甲】写 200 → ✓ ok 往【乙】写 300 → ✓ ok
读甲 → 200 读乙 → 300 都能用,但两张 ok 里必有一张是假的
6.3 「三选二」这个说法哪里不准确
常见读法:“C、A、P 三个里挑两个。” —— 听起来像是可以主动放弃 P。
但 P(网络分区)不是选项,是会发生的事实。 只要数据在多台机器上,网线就会断、交换机就会挂。所以你永远"选了 P"。
真实的选择只有一个,而且只在断网那一刻才发生:
网线断了 → 拒绝服务(保 C 弃 A) 还是 返回可能过期的数据(保 A 弃 C)?
| 业务 | 倒向 | 因为 |
|---|---|---|
| 转账、库存扣减、支付 | C | 宁可报错,不能算错 |
| 点赞数、浏览量、推荐列表 | A | 看到旧数字无所谓,看不到页面才是事故 |
| 用户资料、商品详情 | 看用途 | 展示可以旧,拿去做逻辑判断就不行(原文"非敏感场景才能读从节点"是同一个判断) |
原文说"通常不放弃 A,于是在 C 和 P 之间权衡"——更准确的读法是:P 躲不掉,你只能预先决定分区发生时倒向哪边。
而 AP 那个结局正好撞上 36 讲的底线:“服务端不允许 ok 是假的”。
选 AP 就是明知会产生假 ok,赌这类数据假一会儿没关系。
这个赌注只能由业务下,架构师不能替业务下。
七 · ★ 引申:把四节接起来——跨分片的事务有多难
⚠️ 本节是引申,不是原文内容。 但它是这四节唯一的交汇点,也是原文那句"分布式数据库很讨厌事务"的落地。
第三节那把"提交时才拿的全局锁",前提是所有数据都在同一台机器的同一块内存里。
一旦分片(第五节),事务要改的两行落在了 3 号机和 7 号机:
单机事务: with 全局锁: 比对版本号 → 微秒级
跨片事务: 3号机: 准备好了吗? ──网络──▶ 等
7号机: 准备好了吗? ──网络──▶ 等
两边都说好了 → 再各发一次"提交"
← 这就是两阶段提交 2PC
问题出在中间那段等待:
- 网络往返把"微秒级"变成"毫秒级",锁的时间涨了上千倍
- 更糟的是,如果协调者在"都说好了"和"发提交"之间挂掉——两台机器都锁着资源,谁也不敢动(2PC 的阻塞问题)
- 而网络分区(第六节)保证了这件事一定会发生
所以工程上最实用的一条不是"用什么分布式事务算法",是"想办法别跨分片":
按用户 ID 分片,让一个用户的所有数据落在同一片——转账在同行内、订单和订单项在一起。
这样绝大多数事务退化回单机事务,第三节那套原封不动能用。这也回头解释了 5.5 那张表为什么重要:分片键怎么选,决定的不只是数据搬多少,更决定了你的事务能不能留在一台机器上。
八 · 验收
| # | 问题 | 答案在 |
|---|---|---|
| 1 | 主从和分片各解决什么问题?能不能互相替代? | 四 / 五 |
| 2 | 哈希分片 3→4 台要搬 75% 的数据,为什么这么多?范围分片搬得少,代价换到哪去了? | 五 |
| 3 | 乐观锁"提交时锁住整个数据库",跟"一把大锁"到底差在哪? | 三 |
| 4 | 为什么事务要记录读条件,而不是读过哪些 KEY? | 三 |
| 5 | 选主为什么要超过半数?为什么 4 个节点不如 3 个? | 四 |
| 6 | "CAP 三选二"这个说法哪里不准确? | 六 |
| 7 | 为什么说 KV 既是数据库的"特例"又是它的"基础"? | 一 |
| 8 | CAP 讨论的是分片之间还是主从之间的事?为什么? | 六 |
答案
1. 不能替代,两件事垂直。
主从 = 复制,每台存全量,解决"机器会坏"(可用性 + 持久性);
分片 = 切分,每台存一部分,解决"放不下 / 扛不住"(容量 + 吞吐)。
原文"分片服务器一起构成一个单副本的数据库"就是提示:分片不产生副本。
真实系统是 3 分片 × 3 副本 = 9 台机器、3 份数据。
2. 因为 hash(key) % 3 和 % 4 的结果几乎无关,机器数一变全班重新排座位。
范围分片扩容只是劈开一片,所以只搬那片的一半(16.7%)。
代价换成了两样:分布不均(看 key 怎么长)和热点——
key 只要是递增的(时间戳、订单号),新写入永远只压最后一片。
一致性哈希是折中:搬 1/N(23.4%),但也没有范围查询的能力。
3. 差别在锁多久,不在锁多大。
“一把大锁"横跨整个事务,期间要做磁盘 IO、跑业务逻辑、甚至等网络往返;
乐观锁只在提交那一瞬间拿锁,做一次纯内存的版本号比对,微秒级。
原文说的"把锁数据库的粒度降到最低”,降的是时间粒度。
4. 防幻读。只记 KEY 的话:我读了"age>17 的 3 个人"并据此决策,
另一个事务插入了一个 age=20 的新人。提交时检查我读过的那 3 行——一个字都没改过,通过。
可结论已经错了(现在是 4 个人)。
我读过的行没变,但我读到的那个"集合"变了。 只有记下条件本身,新插入的行才会落进检查范围。
5. 为了防脑裂——网络一断,两边各选一个主、各自接受写,数据从此分叉。
"过半数"让它在数学上不可能:两个都超过半数的集合必然相交,而一个节点不能同时投两个主。
4 个节点过半需要 3 票,最多只能挂 1 台,和 3 个节点完全一样——
多花一台机器的钱,多一台会坏的机器,容错能力一点没涨。
6. 因为 P 不是一个可以挑的选项,它是会发生的事实。
只要数据在多台机器上,网络就会断。所以你永远"选了 P"。
真实的选择只在断网那一刻发生:拒绝服务(保 C 弃 A),还是返回可能过期的数据(保 A 弃 C)。
而且这个选择由业务下:钱和库存倒向 C,点赞和展示倒向 A。
7. 使用上是特例:数据库可以建多个索引,KV 只有唯一索引。
实现上是基础:数据库的每一个索引都是一个 KV。
一张有两个索引的表 = 主键KV(存整行)+ 两个索引KV(存"索引值 → 主键")。
WHERE 邮箱='x' 实际是两次 KV 查找。数据库 ≈ 一组 KV + 一个查询规划器。
8. 主从之间。 分片存的是不同的数据,各管各的一块,本来就不一样,无所谓"听谁的";
主从存的是同一份数据的复制品,必须一样——"一致性"这个问题只在这里成立。
分片只是让网络分区更常见(机器越多网线越容易断),但它本身不制造一致性问题。
下一步
38 讲:对象存储——存储中间件清单上的下一项。
回看:36 讲 · 存储中间件的由来(四堵墙 · 存储即数据结构)
