每日好书推荐:《设计数据密集型应用》
推荐日期:2026-05-07
作者:Martin Kleppmann
英文原名:Designing Data-Intensive Applications
适合谁读:后端开发、架构师、数据库/中间件工程师、正在从“会写业务代码”迈向“能设计可靠系统”的程序员。
一句话推荐:这本书不是教你背某个数据库参数,而是教你看懂数据系统背后的取舍:为什么会慢、为什么会丢、为什么会不一致,以及工程上怎么尽量把风险压住。
这本书到底在讲什么?
《设计数据密集型应用》的核心问题很朴素:当一个系统的数据越来越多、用户越来越多、机器越来越多,怎样还能让它可靠、可扩展、可维护?很多人一提架构就想到“上缓存、上分库分表、上消息队列、上微服务”,但这本书反复提醒你:没有银弹。每一种技术都在解决一类问题,同时也在制造另一类问题。
作者把数据系统拆成三层来看。第一层是基础:什么叫可靠、可扩展、可维护;数据模型怎么选;数据库内部怎样存取数据;系统升级时数据格式怎么演进。第二层是分布式数据:复制、分区、事务、分布式系统的各种坑、一致性与共识。第三层是衍生数据:批处理、流处理,以及未来的数据系统应该如何把正确性、可演进性和可运维性结合起来。
用大白话说:这本书教你别只会“堆组件”,而要知道组件之间的因果关系。缓存会带来一致性问题,复制会带来延迟和冲突,分区会带来热点和跨分区事务,异步消息会带来重复和乱序,强一致会带来性能和可用性的成本。架构设计的本质,就是在这些矛盾里做清醒选择。
第一部分:数据系统的基础
第 1 章:可靠、可扩展、可维护的应用
开篇先立标准:一个好系统至少要做到可靠、可扩展、可维护。可靠不是“永远不出错”,而是硬件会坏、软件会崩、人会误操作时,系统仍能尽量继续提供正确服务。可扩展不是“机器越多越好”,而是负载变大时,你知道瓶颈在哪里,并能用合适方式扩容。可维护不是“代码看起来整洁”这么简单,而是未来的人能理解它、修改它、排查它。
作者强调,很多事故不是单点故障,而是多个小问题叠加:一个慢查询、一个错误配置、一次重试风暴、一个过期缓存,最后把系统压垮。所以工程师不能只盯功能,还要思考故障模型、监控指标、容量规划和复杂度控制。复杂度是维护性的天敌,抽象、模块化和清晰的数据流,都是为了让系统在人脑里装得下。
第 2 章:数据模型与查询语言
这一章讲“你怎么描述世界”。关系模型把数据放进表里,用行、列、外键表达关系;文档模型把相关数据聚成一个文档;图模型适合表达复杂关系,比如社交网络、推荐系统、知识图谱。没有哪种模型永远最好,关键看你的数据访问方式。
如果数据经常需要多表关联,关系数据库很稳;如果一个对象天然是一整块,比如用户配置、文章内容,文档数据库更顺手;如果重点是“谁和谁有什么关系”,图数据库会更自然。作者还比较了声明式查询和命令式查询:声明式查询只说“我要什么”,把“怎么做”交给优化器;命令式查询则一步步告诉机器怎么取。声明式的好处是更容易优化、更容易并行,也更适合长期演进。
第 3 章:存储与检索
数据库不是魔法,本质上是在磁盘和内存里组织数据,让写入和查询尽量快。本章讲两大类存储引擎:日志结构和页结构。日志结构的思路是追加写,先把数据顺序写下来,再通过索引找到它;LSM Tree、SSTable 就是代表。页结构的代表是 B-Tree,把数据按页组织,适合范围查询和稳定更新。
这里的核心取舍是:写快、读快、空间省,三者很难同时拉满。追加日志写入快,但后台合并会消耗资源;B-Tree 读写都比较均衡,但随机写可能成本高。索引也不是越多越好,索引能加快查询,却会拖慢写入、占用空间。作者还讲了 OLTP 和 OLAP:前者服务在线交易,追求低延迟和高并发;后者服务分析,追求大范围扫描和聚合。行式存储适合事务,列式存储适合分析。
第 4 章:编码与演进
系统会升级,数据格式也会变。问题是:新老版本经常会同时存在,今天上线的新服务,可能还要读取昨天旧服务写的数据;旧客户端也可能读到新服务写的数据。所以一个数据格式要考虑向后兼容和向前兼容。
作者比较了 JSON、XML、Protocol Buffers、Thrift、Avro 等编码方式。文本格式可读性强,但体积大、类型约束弱;二进制格式更紧凑、更适合机器处理,但需要 schema。真正重要的是演进规则:新增字段、删除字段、修改字段类型,都要小心。服务之间通信也分几类:数据库共享、REST/RPC、消息队列。共享数据库耦合强,RPC 像本地调用但隐藏了网络不可靠性,消息队列则把发送方和接收方解耦,但引入异步、重复、顺序等问题。
第二部分:分布式数据
第 5 章:复制
复制就是把同一份数据放到多台机器上。好处很明显:一台坏了还有别的机器;读请求可以分摊到多个副本;用户离哪个机房近就读哪个机房。但复制的麻烦也很明显:多个副本怎么保持一致?
单主复制最常见:所有写入先到主库,再同步到从库。它简单好理解,但主库是瓶颈,也可能出现复制延迟。多主复制允许多个地方写,适合多机房或离线编辑,但会出现冲突,需要合并策略。无主复制让客户端写多个副本、读多个副本,常用 quorum 思路,即写入 W 个副本、读取 R 个副本,只要 W+R 大于总副本数 N,理论上能读到较新数据。但现实里还有时钟、网络、并发写入、读修复、反熵等复杂问题。
这一章最重要的提醒是:复制不是“多拷贝一份”这么简单,它本质上是在可用性、延迟和一致性之间做权衡。
第 6 章:分区
当一台机器装不下或扛不住,就要把数据分到多台机器上,这就是分区,也叫分片。分区的目标是让数据和负载尽量均匀分布。常见方法有按 key 范围分区和按 key 哈希分区。范围分区适合范围查询,但容易出现热点,比如按时间写入时最新时间段压力最大;哈希分区更均匀,但范围查询不方便。
分区之后,二级索引会变复杂。局部索引只在本分区维护,写入简单,但查询可能要扫多个分区;全局索引查询快,但更新成本更高,维护一致性更难。分区还涉及再平衡:新增机器、机器故障、数据增长时,怎样迁移数据而不影响服务。作者不建议简单用“hash mod N”,因为机器数量变化会导致大量数据迁移,更好的方式是固定分区数或使用一致性哈希一类方案。
第 7 章:事务
事务是数据库给程序员的“安全垫”。没有事务,应用要自己处理各种半成功半失败的情况,很容易乱。传统事务强调 ACID:原子性、一致性、隔离性、持久性。这里的一致性不是分布式一致性,而是应用定义的业务约束,比如余额不能凭空消失。
最难的是隔离性。很多数据库为了性能,并没有默认提供真正的串行化,而是提供读已提交、快照隔离等较弱级别。弱隔离可能引发脏读、脏写、读倾斜、丢失更新、写倾斜、幻读等问题。作者用很多例子说明:你以为两段代码不会互相影响,但并发时它们可能一起破坏约束。
解决办法有悲观锁、原子写、显式锁、可串行化快照隔离、真正串行执行等。核心观点是:事务不是过时技术,很多“为了扩展性放弃事务”的系统,最后只是把复杂性推给了应用层。能用事务解决的问题,不要轻易自己造一套脆弱补丁。
第 8 章:分布式系统的麻烦
这一章很扎心:分布式系统难,是因为你永远不能完全确定发生了什么。网络可能延迟、丢包、重复、乱序;机器可能停顿;进程可能被 GC 卡住;时钟可能不准;你以为对方死了,也可能只是网络慢。
单机程序里,函数调用要么返回,要么报错;分布式系统里,请求超时不代表失败,它可能已经成功,只是响应丢了。于是重试可能导致重复写,超时设置太短会误判故障,太长又会影响恢复速度。时钟也不可靠:物理时钟会漂移,逻辑时钟只能表达因果顺序,不能表达真实时间。
作者强调,系统设计要承认“不确定性”。不要依赖完美网络、完美时钟、完美节点。要通过幂等、唯一请求 ID、租约、防护令牌、合理超时、监控和故障注入来降低风险。
第 9 章:一致性与共识
这一章是分布式系统的核心硬菜。线性一致性给人的感觉最像“只有一份数据”:一旦写入成功,之后所有读取都能看到它。但线性一致代价很高,尤其跨机房时会带来更高延迟,并在网络分区时牺牲可用性。
作者解释了顺序保证、因果关系、全序广播、共识算法之间的关系。共识就是让多个节点对某件事达成一致,比如谁是主节点、某条日志排第几位、某个事务是否提交。Paxos、Raft、Zab 这类算法就是为了解决这个问题。它们通常需要多数派同意,因此能容忍少数节点故障,但不能在多数节点不可用时继续工作。
两阶段提交用于原子提交,但协调者故障可能让参与者卡住;容错共识系统能解决一部分问题,但实现和运维都更复杂。这里的关键观点是:一致性不是一个开关,而是一组不同强度的保证。系统应该明确自己需要哪种保证,而不是用模糊的“最终一致”糊弄过去。
第三部分:衍生数据
第 10 章:批处理
批处理就是定期处理一大堆已有数据,比如日志分析、报表、推荐特征生成。MapReduce 的思想很简单:Map 负责把输入拆开处理,Reduce 负责按 key 聚合结果。它的优势是吞吐量高、容错强、适合海量离线数据。
作者把 Unix 管道和 MapReduce 放在一起讲,是为了强调一种设计美学:每个程序做一件事,用清晰输入输出串起来。批处理系统的价值不只是算得快,还在于可重跑。只要输入不变,任务失败后可以重试,输出可以重新生成,这让它比很多在线系统更容易保证正确性。
但批处理也有缺点:延迟高。你可能几个小时后才看到结果,不适合实时反馈。因此后面自然引出流处理。
第 11 章:流处理
流处理可以理解为“数据一来就处理”。消息队列、事件日志、变更数据捕获,都是流处理的重要基础。它适合实时监控、实时推荐、风控、搜索索引更新、缓存刷新等场景。
流处理最难的不是把消息消费掉,而是处理时间、顺序和重复。事件发生时间和处理时间可能不同;消息可能乱序到达;消费者可能崩溃后重复处理同一条消息。所谓 exactly-once 很诱人,但现实里通常要靠幂等写入、事务性输出、检查点、去重 ID 等机制配合完成。
作者还讲了事件溯源和 CDC。事件溯源不是只保存当前状态,而是保存导致状态变化的事件;CDC 则把数据库变化作为事件流输出。它们都体现了一个思想:不要只看最后结果,也要保留变化过程。这样系统更容易同步、重建和审计。
第 12 章:数据系统的未来
最后一章把全书收回来:未来的数据系统不该是一个巨大的黑盒,而应该由多个可组合、可演进、可验证的组件构成。传统上,数据库负责存储和查询;缓存负责加速;搜索引擎负责全文检索;数仓负责分析;消息队列负责异步。问题是这些系统之间会产生大量派生数据,而派生数据是否正确、是否及时、是否可恢复,往往决定了系统质量。
作者提出一个重要方向:把数据库、流处理和批处理结合起来,用事件日志作为连接系统的骨架。原始数据作为事实来源,索引、缓存、报表、推荐结果都可以看作从事实派生出来的视图。只要派生逻辑清楚,数据就能重建,系统也更容易演进。
本章还讨论了端到端正确性、幂等、约束、审计、隐私和伦理。技术系统不是只追求吞吐量,它也会影响人的生活。数据越多,越要谨慎处理权限、解释性和责任边界。一个成熟工程师不仅要问“能不能做”,还要问“做错了怎么办”“谁会受影响”。
这本书最值得带走的 10 个观点
- 架构没有免费午餐。每个组件都带来收益,也带来新的故障模式。
- 可靠性要按故障来设计。硬件、网络、软件、人都会出错,系统要默认它们会出错。
- 扩展性要靠指标说话。先知道瓶颈是 CPU、内存、磁盘、网络还是锁竞争,再谈优化。
- 数据模型决定系统形状。关系、文档、图模型不是信仰问题,而是访问模式问题。
- 索引不是越多越好。读写性能、空间成本、维护复杂度必须一起算。
- 复制和分区会放大一致性问题。多副本、多机器之后,“读到什么”就不再简单。
- 事务是降低复杂度的工具。不要为了追潮流轻易把一致性问题丢给业务代码。
- 分布式系统里超时不等于失败。请求可能成功、失败、卡住,也可能只是响应丢了。
- 事件日志是连接系统的好方式。它能让多个派生视图从同一事实来源构建出来。
- 正确性比炫技更重要。一个系统跑得快但悄悄算错账,价值反而是负的。
怎么读这本书更有效?
如果你是初学者,不建议一口气硬啃。可以先按问题来读:为什么数据库要有索引?为什么主从复制会延迟?为什么分库分表后事务变难?为什么消息队列会重复消费?带着这些问题读,会比单纯看概念轻松很多。
如果你已经做后端开发,可以把它当成架构排雷手册。每读一章,就对照自己的系统想一遍:我们有没有复制延迟?有没有热点分区?有没有弱隔离导致的并发 bug?有没有重复消息?有没有不可重建的缓存和索引?这些问题比背概念更有价值。
如果你做游戏后台、实时服务或高并发系统,这本书尤其值得反复读。游戏服务经常面对状态同步、排行榜、账号数据、道具交易、日志分析、跨服活动等问题,这些背后都绕不开复制、分区、事务、一致性和流处理。
总结
《设计数据密集型应用》真正厉害的地方,是它不把技术讲成工具清单,而是讲成一套思考方式。它让你看到数据库、缓存、队列、搜索、流处理、批处理背后的共同问题:数据如何产生、如何存储、如何传播、如何被派生、如何保持正确。
读完这本书,你可能不会立刻写出一个分布式数据库,但你会更清楚地知道:什么时候该用事务,什么时候能接受最终一致,什么时候要警惕复制延迟,什么时候分区会制造热点,什么时候消息队列不是解耦神器而是复杂度搬运工。对后端工程师来说,这种判断力比记住某个框架 API 更值钱。