本篇要回答的问题:服务端的业务状态和桌面有何本质不同?为什么会催生存储中间件,它在服务端为何地位如此重要?为什么说"存储即数据结构,存储中间件就是元数据结构"?
上一讲讲完服务端多出的第一类基础软件——负载均衡。本讲讲第二类——存储(DB/Storage)。切入点是"业务状态":从数据视角对比桌面与服务端,进而推导存储中间件的由来。
觉得抽象、抓不住"由来"时,可先跳到文末的 [[#为什么:把这篇倒过来推一遍]]——那节从一个四行的 Python 服务出发,把结论重新推了一遍。
业务状态:两者其实是同一个状态机
桌面与服务端都可看作状态机,差别只在驱动事件:
| 驱动事件 | 状态转移公式 | |
|---|---|---|
| 桌面程序 | 用户交互事件 | 业务状态ᵢ₊₁ = F(用户交互事件ᵢ, 业务状态ᵢ) |
| 服务端程序 | 网络 API 请求 | 业务状态ᵢ₊₁ = F(网络 API 请求ᵢ, 业务状态ᵢ) |
最大差别在于业务状态的表示:
| 业务状态如何表示 | 持久化 | |
|---|---|---|
| 桌面 | 内存中的数据结构(Model 层那棵以 Document 为根的 DOM 树) | 多数无需持久化;需要时靠明确的用户操作(如 Ctrl+S)整体存盘 |
| 服务端 | 不能放内存——程序一挂数据就丢 | 每次 API 请求后都须可靠持久化 |
核心原则(比"大规模+24×7"更重要):坚决不能丢失用户的数据,即他认为已完成的业务状态。服务端对用户是黑盒,用户一旦收到 API 成功反馈,就认定这个成功是确认的。
存储中间件与容灾级别
服务端自己做持久化为什么难(远比桌面难):
| 难点 | 原因 |
|---|---|
| 持久化时间必须极短 | 多用户共享,持久化时若别的请求排队,轻则吞吐差、重则用户眼里"停止服务",须短到无感知 |
| 必须增量式 | 多租户状态量巨大(100 万用户 × 100K = 100G),不能像桌面"每次重新生成新文件" |
让每个服务端开发者各自考虑持久化,代价过高 → 存储中间件应运而生。历史上第一个存储中间件是数据库(1974 年 IBM System R)。桌面很少用数据库,少数需增量持久化的场景才用(如微信本地聊天记录用嵌入式 SQLite)。
容灾级别随互联网普及逐步抬高:
| 阶段 | 诉求 | 方案 |
|---|---|---|
| 早期 | 高性能、稳定、经验证 | 单机数据库,低峰期离线备份即可 |
| 进阶① | 防单机故障/数据丢失 | 主从结构(多机热备) |
| 进阶② | 规模可伸缩,突破单机容量上限 | 分布式数据库(早期靠手工分库分表) |
| 进阶③ | 防机房级灾难(网络中断、地震) | 两地三中心、跨机房容灾灾备 |
存储即数据结构 → 存储中间件就是"元数据结构"
核心论断:存储即数据结构。存储中间件就是"元数据结构"。
为什么在服务端实现一个数据结构如此之难:
| 论据 | 说明 |
|---|---|
| 基于外存而非内存 | 桌面数据结构基于内存、实现易;服务端每次状态改变都要持久化,核心数据结构都基于外存 |
| 稳定性/并发要求极高 | 服务端的伸缩能力完全取决于存储的伸缩能力——业务服务器多无状态、加机器容易;存储压力大却可能要重新划分和搬迁数据 |
| 品控极难 | 内存里实现一个 KV(Map/Dictionary)几十分钟搞定;但服务端 KV 极其复杂,写出来也不敢直接上,需海量测试,上线初期还常用"双写"(同时用成熟存储 + 新存储)兜底 |
正因如此,服务端所有业务涉及的数据结构都需抽象成存储中间件。"元数据结构"是作者自造词:数据结构的种类非常有限、最好理论可被证明,有了这些基本数据结构,所有业务需求都能高效实现。
今天常见的存储中间件(不完整、目前不可枚举):
| 存储中间件 | 英文 |
|---|---|
| 键值存储 | KV-Storage |
| 对象存储 | Object Storage |
| 数据库 | Database |
| 消息队列 | MQ |
| 倒排索引 | SearchEngine |
为什么:把这篇倒过来推一遍
原文是"结论式"的——先给状态机模型,再宣布存储中间件"应运而生"。这一节反过来:从一个能跑的四行 Python 服务出发,看它是怎么被一步步逼出中间件的。
起点:业务状态就是这个 dict
balance = {} # ← 这就是"业务状态"
def handle(req): # ← 这就是那个 F
balance[req.user] += req.amount
return "ok"
业务状态ᵢ₊₁ = F(请求ᵢ, 业务状态ᵢ) 说的就是这四行。桌面程序一模一样,只是 req 从"网络请求"换成"用户点了下鼠标"。
真正的差别只有一个:balance 活在内存里。桌面上无所谓——Word 崩了最多丢没存盘的几段,而且用户自己知道他没按 Ctrl+S。服务端不行:用户看到 "ok" 就认定事情办完了;进程一挂 dict 没了,用户却攥着一张"成功"的回执。
所以这篇的起点不是"服务端需要数据库",而是——服务端不允许那个 ok 是假的。
自己扛持久化,会依次撞上四堵墙
第一版补丁,每次请求后存盘:
def handle(req):
balance[req.user] += req.amount
json.dump(balance, open("state.json", "w")) # 存盘
return "ok"
看着能用。然后会依次撞上这四堵墙:
| # | 撞上什么 | 被逼出什么 |
|---|---|---|
| 墙 1 | 100 万用户 × 100K = 100G,改一个人的余额重写了 100G。桌面能这么干(.docx 才几 MB,且用户按了 Ctrl+S 自己知道要等),服务端不能 | 增量式存储:只写变化的那一小块 |
| 墙 2 | 只写一小块,就得知道那块在文件的第几个字节;这张"用户→偏移量"的表本身也要存盘、也会随数据增长而分裂 | 索引 / B 树 |
| 墙 3 | 两个请求同时 json.dump 互相覆盖;写到一半进程被 kill,文件半新半旧直接损坏,连之前存好的数据一起赔进去 |
WAL(先把要做的事追加进日志,再改主文件,崩了照日志重放)与事务 |
| 墙 4 | 这台机器本身会坏:硬盘、机房断网、地震 | 主从热备 → 分库分表 → 两地三中心(即上一节那张容灾级别演进表) |
走完这四步,手里那坨代码已经就是一个数据库了。
这才是"由来":不是某天有人发明了数据库,而是每个写服务端的人都会被这四堵墙逼着走同一条路,走出来的东西还长得一模一样。既然如此,不如抽成一个独立进程,全世界共用一份、并被千万个生产环境验证过。1974 年的 IBM System R 就是第一个被这么抽出来的东西。
作用也是一句话:把"可靠 + 增量 + 高并发 + 不停服地保存状态"这件极难的事,从业务代码里整个搬走,让剩下的活儿变回写那个纯函数
F。
"存储即数据结构"倒过来读就懂了
别顺着读,倒着读:存储中间件的清单,其实就是内存数据结构清单的服务端版本。
| 你想要的东西 | 内存里 | 服务端对应的中间件 |
|---|---|---|
| dict | d = {} |
KV 存储(Redis、etcd) |
| 一大坨 bytes | open(f, 'wb').write() |
对象存储(S3、七牛) |
| 一堆记录 + 按条件查 | list of dataclass + 遍历 | 数据库(MySQL、PG) |
| 队列 | queue.Queue() |
消息队列(Kafka、RabbitMQ) |
倒排表 {词: [文档]} |
二十行代码 | 搜索引擎(Elasticsearch) |
左边一行代码,右边一个团队干几年。差的不是功能,是"落盘 + 并发 + 挂了不丢 + 能扩容"——也就是上面那四堵墙。
“元数据结构"于是可以这么理解:右边这一列不是无限长的,就像语言的内置类型也就那么几个。设计业务时脑子里该想的是"我这儿需要一个能持久化的 dict”,而不是"我要不要上 Redis"——先定数据结构,中间件只是它的服务端实现。
为什么结论必然落在"扛不住都是存储扛不住"
再回到 F:业务服务器跑的是 F(请求, 状态),它自己不持有状态(状态全在存储里),所以是无状态的——压力大了,复制一台一模一样的挂到负载均衡后面即可。
存储不行,存储就是状态本身。加一台机器意味着把现有数据切开、搬一半过去,搬的过程中还要继续对外服务、还不能丢不能错。完全不同的难度量级。
整条链路上唯一那个"加机器解决不了"的环节就是存储,它必然是瓶颈。上一讲的负载均衡管无状态那半边怎么随便加机器,这一讲的存储中间件管有状态那半边——两讲拼起来才是完整的服务端骨架。
总结
本讲从"桌面与服务端都是状态机、差别只在业务状态的表示"切入,推出服务端必须可靠、增量、低延迟地持久化业务状态,从而催生了存储中间件,并随互联网演进抬高容灾级别(主从→分布式→两地三中心)。核心论断是"存储即数据结构,存储中间件就是元数据结构":服务端数据结构基于外存、品控极难,故所有业务数据结构都要抽象为经过千锤百炼的存储中间件。几乎所有服务端扛不住压力,根因都是存储没扛住。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| 业务状态 | 状态机的当前状态;桌面在内存,服务端必须可靠持久化 |
| 不丢数据原则 | 用户收到成功反馈即视为确认,服务端坚决不能丢这份业务状态 |
| 存储中间件 | 把业务状态持久化的能力抽象出来,避免每人各自造轮子 |
| 容灾级别演进 | 单机备份 → 主从热备 → 分布式可伸缩 → 两地三中心跨机房 |
| 存储即数据结构 | 存储本质是基于外存的数据结构 |
| 元数据结构 | 种类有限、最好可证明完备的一组基本存储数据结构 |
| 双写 | 新存储上线初期同时写成熟存储兜底,保品控 |
一句话速记
桌面与服务端都是状态机,唯一本质差别是业务状态怎么存——服务端因"绝不丢数据 + 多租户 + 低延迟"必须把持久化抽象成存储中间件(元数据结构);而服务端的伸缩与稳定,几乎完全由存储决定。
几条值得记住的判断
- 不丢数据 > 高性能:用户认为已完成的状态,服务端必须可靠保住。
- 伸缩能力取决于存储:业务服务器易扩,存储扩容要搬数据,是真正瓶颈。
- 存储品控至关重要:自研存储再急也要海量测试 + 双写兜底。
思考题
作者说"存储中间件就是元数据结构",并坦言今天还没有系统理论能告诉我们"有了哪些数据结构就完备了"。结合你接触过的业务,你认为 KV、对象存储、数据库、MQ、倒排索引之外,还有哪些不可或缺的"元数据结构"?它们能否被更基本的几种组合出来?



