最近读完了Martin Kleppmann的《设计数据密集型应用》(Designing Data-Intensive Applications,简称DDIA),这本书可以说是后端工程师的必读书,被很多人称为分布式系统和数据库领域的圣经。读完之后确实受益匪浅,很多以前模糊的概念都清晰了,很多以前知其然不知其所以然的技术点都理解了背后的原理。

今天这篇文章就来梳理一下这本书的核心观点,分享给大家。这本书内容很多,分了三个大部分:数据系统基础、分布式数据、派生数据,每个部分都有很多干货。我会尽量把核心观点提炼出来,让没读过的朋友也能快速了解这本书的精华。

一、关于这本书和作者

先简单介绍一下这本书和作者。

作者Martin Kleppmann是分布式系统领域的专家,曾经在LinkedIn和Confluent工作,参与过Apache Samza等分布式项目的开发,现在是剑桥大学的研究员。他不仅工程经验丰富,而且学术功底深厚,这本书就是他结合工程实践和学术研究写的。

这本书的英文原版2017年出版,中文版2018年出版,出版之后就广受好评,被很多公司和工程师推荐。它不是一本教你怎么用某个数据库的书,而是讲数据系统背后的基本原理和设计思想,不管你用的是MySQL、PostgreSQL、MongoDB、Redis还是Cassandra,这些原理都是通用的。

这本书的特点是深入浅出,把复杂的分布式系统概念讲得很清楚,而且有很多实际的例子和对比,不会像纯学术书那样枯燥。它也不偏向某个特定的技术,而是客观地分析各种技术的优缺点和适用场景,让读者能根据自己的需求做出合理的技术选型。

接下来进入正题,梳理核心观点。

二、第一部分:数据系统基础

第一部分讲的是数据系统的基础,包括数据模型、存储引擎、索引、编码等。这部分是理解后续内容的基础。

1. 数据模型的选择是最重要的决策之一

书里一开始就强调,数据模型是软件设计中最重要的决策之一,因为它决定了我们思考问题的方式,也决定了系统的能力和局限性。

常见的数据模型有关系模型(SQL)、文档模型(MongoDB等)、图模型(Neo4j等)、键值模型(Redis等)、列式模型(Cassandra等)。每种模型都有自己的适用场景,没有银弹。

关系模型的优势是数据结构化,支持复杂的关联查询和事务,适合数据关系复杂、需要强一致性的场景。文档模型的优势是数据结构灵活,适合数据结构不固定、嵌套数据多的场景,但是关联查询能力弱。图模型适合数据之间关系复杂、需要多跳查询的场景,比如社交网络、推荐系统。

书里特别提到了对象关系不匹配的问题,也就是应用层的对象模型和关系模型之间的阻抗不匹配,这也是ORM和文档数据库出现的原因之一。但是文档数据库也不是万能的,当数据之间的关联很多时,文档模型反而不如关系模型方便。

核心观点是,要根据业务的数据特点和查询模式选择合适的数据模型,不要盲目追新,也不要觉得关系模型就一定过时了。很多时候关系模型依然是最好的选择。

2. 存储引擎和索引的原理

这一部分讲了数据库底层是怎么存储数据和建立索引的,这是理解数据库性能的关键。

常见的存储引擎有两种:日志结构(log-structured)和页结构(page-oriented,也就是B树)。

日志结构的代表是LSM树(Log-Structured Merge Tree),LevelDB、RocksDB、Cassandra、HBase都用了这种结构。它的原理是写入的时候先写内存中的memtable,满了之后刷到磁盘成为SSTable,后台定期合并SSTable。读取的时候从memtable和各个SSTable里找,用布隆过滤器优化。LSM树的优势是写入性能好,因为写入是顺序写,而且有合并机制,适合写多读少的场景。

B树是最传统的索引结构,MySQL的InnoDB、PostgreSQL都用B树。它把数据分成固定大小的页,通过树形结构组织,查找的时候从根节点一层层往下找。B树的优势是读取性能稳定,适合读多写少的场景,但是写入的时候可能需要页分裂,而且随机写比较多。

书里详细对比了这两种结构的优缺点,以及各种优化手段,比如写前日志(WAL)、压缩、分区、二级索引等。理解了这些原理,就能明白为什么不同的数据库性能特点不一样,也能根据业务场景选择合适的存储引擎。

还有一个重要的观点是,索引不是越多越好,索引会增加写入的开销和存储成本,而且不是所有查询都能用到索引。要根据实际的查询模式建立合适的索引,并且定期分析慢查询,优化索引。

3. 编码和序列化

这一部分讲了数据的编码和序列化,包括JSON、XML、Protocol Buffers、Thrift、Avro等。

核心观点是,数据的编码方式会影响系统的兼容性、性能和可维护性。选择编码方式的时候要考虑向前兼容和向后兼容,也就是新版本的代码能不能读旧版本的数据,旧版本的代码能不能读新版本的数据。

文本格式(JSON、XML)的优势是人类可读,调试方便,但是体积大,性能差,而且没有严格的schema,字段类型容易出问题。二进制格式(Protocol Buffers、Thrift、Avro)的优势是体积小,性能好,有严格的schema,但是人类不可读,调试不方便。

书里特别推荐了Avro,因为它的schema演化设计得很好,支持动态schema,适合大数据场景。而Protocol Buffers和Thrift更适合RPC场景。

还有一个重要的观点是,数据格式的演化是长期的,系统会不断升级,数据会在不同版本的系统之间流转,所以从一开始就要考虑数据格式的兼容性,不要把字段名和类型写死,要预留演化的空间。

三、第二部分:分布式数据

第二部分是这本书的核心,讲的是分布式数据系统,包括复制、分区、事务、一致性、分布式系统的麻烦等。这部分内容最有价值,也最烧脑。

1. 复制的三种模式

复制是分布式系统最基础的功能,目的是让数据在多个节点上有副本,提高可用性和读取性能。常见的复制模式有三种:主从复制、多主复制、无主复制。

主从复制是最常见的,一个主节点负责写入,从节点复制主节点的数据,负责读取。MySQL、PostgreSQL、MongoDB都支持主从复制。它的优势是简单,一致性好控制,缺点是主节点是单点,写入性能受限于主节点。

多主复制是多个节点都能接受写入,每个节点把写入同步给其他节点。适合多数据中心部署,或者需要离线写入的场景。它的优势是写入性能好,没有单点,缺点是冲突解决复杂,可能出现写入冲突。

无主复制是没有主节点,任何节点都能接受写入,客户端写入多个节点,读取多个节点,通过quorum(法定人数)保证一致性。Dynamo、Cassandra、Riak用的是这种模式。它的优势是高可用,写入性能好,缺点是一致性弱,可能出现数据不一致,需要后台修复。

书里详细讲了每种模式的原理、优缺点、适用场景,以及各种细节问题,比如复制延迟、复制延迟带来的问题(读己之写、单调读、一致前缀读)、冲突解决策略等。

核心观点是,没有完美的复制模式,每种模式都有取舍,要根据业务的需求(一致性要求、可用性要求、性能要求、部署场景)选择合适的模式。而且复制不是万能的,复制延迟会带来各种一致性问题,要理解这些问题,在应用层做相应的处理。

2. 分区(分片)

分区就是把大数据集分成多个部分,分布在不同的节点上,解决单节点存储和性能瓶颈。常见的分区方式有键范围分区和哈希分区。

键范围分区是按key的范围分区,类似字典的页码,适合范围查询,但是容易出现热点(比如按时间分区,最新的分区写入多)。BigTable、HBase用的是这种方式。

哈希分区是对key做哈希,按哈希值分区,能均匀分布数据,避免热点,但是不支持范围查询。Cassandra、MongoDB的默认分区方式是哈希分区。

书里还讲了分区的再平衡(rebalancing),也就是数据量变化或者节点增减时,怎么重新分配分区。再平衡是一个很复杂的问题,要尽量均匀,还要尽量少移动数据,而且不能影响线上服务。常见的策略有固定数量分区、动态分区、按节点比例分区等。

还有一个重要的话题是请求路由,也就是客户端怎么知道该请求哪个节点。常见的方式有客户端自己路由、通过路由层路由、节点之间转发。分布式系统里通常用一致性哈希或者专门的协调服务(比如ZooKeeper、etcd)来管理分区信息和路由。

核心观点是,分区是分布式系统扩展的基础,但是分区会带来很多复杂性,比如跨分区查询、跨分区事务、再平衡、热点问题等。在做分库分表或者使用分布式数据库之前,要理解这些问题,评估是否真的需要分区,以及选择合适的分区策略。

3. 事务和一致性

这一部分是这本书最烧脑也最有价值的部分,讲了事务的隔离级别、分布式事务、一致性模型等。

首先讲了ACID的真正含义,很多人对ACID有误解。原子性(Atomicity)不是指操作不可分割,而是指事务失败时回滚,要么全部成功要么全部失败。一致性(Consistency)是指事务前后数据满足约束,这个其实是应用层的责任,数据库只是提供保障。隔离性(Isolation)是指并发事务之间互不干扰,这是数据库最复杂的部分。持久性(Durability)是指事务提交后数据不会丢失。

然后详细讲了事务的隔离级别,从弱到强分别是:读未提交、读已提交、可重复读、可串行化。每个隔离级别解决了什么问题,有什么副作用,书里都讲得很清楚。

读已提交解决了脏读(读到未提交的数据),但是不可重复读(同一个事务里两次读同一行结果不一样)和幻读(同一个事务里两次范围查询结果不一样)还是可能发生。

可重复读解决了不可重复读,MySQL的InnoDB通过MVCC和间隙锁还解决了幻读。但是可重复读还是有一些异常情况,比如写偏斜(write skew)。

可串行化是最强的隔离级别,保证并发事务的结果和串行执行一样。实现方式有两阶段锁(2PL)、可串行化快照隔离(SSI)等。可串行化的性能最差,但是能避免所有的并发异常。

书里特别强调了一个观点:很多人以为用了可重复读就没有并发问题了,其实不是,可重复读还是有写偏斜等问题,只有可串行化才能真正避免所有并发异常。但是可串行化性能差,所以实际中大部分系统用的是读已提交或者可重复读,然后在应用层处理并发问题,比如用乐观锁或者唯一约束。

然后讲了分布式事务,包括两阶段提交(2PC)、三阶段提交(3PC)、TCC、SAGA等。分布式事务的核心问题是原子性,也就是多个节点的事务要么全部成功要么全部失败。2PC是最经典的方案,但是有同步阻塞和协调者单点的问题,而且性能差。书里也讲了分布式事务的各种问题和替代方案,核心观点是分布式事务成本高,能不用就不用,尽量通过业务设计避免跨节点事务,如果必须用,要理解各种方案的优缺点和限制。

最后讲了一致性模型,包括线性一致性、顺序一致性、因果一致性、最终一致性等。这是分布式系统最核心也最难理解的概念。核心观点是,分布式系统里一致性和可用性、性能是矛盾的,根据CAP定理,网络分区时只能在一致性和可用性之间选一个。但是CAP定理被过度简化了,实际中要考虑的因素更多,比如延迟、持久性、吞吐量等。要根据业务需求选择合适的一致性级别,不是所有系统都需要强一致性,很多场景最终一致性就够了。

4. 分布式系统的麻烦

这一部分讲了分布式系统的各种不可靠因素,包括不可靠的网络、不可靠的时钟、不可靠的进程等。

网络是不可靠的,会丢包、延迟、分区,所以分布式系统里不能假设网络是可靠的,要处理超时、重试、幂等、网络分区等问题。

时钟是不可靠的,不同节点的时钟可能不一致,而且时钟会跳变(NTP同步),所以不能依赖物理时钟来判断事件的先后顺序,要用逻辑时钟(比如Lamport时钟、向量时钟)或者版本号。

进程是不可靠的,节点可能随时宕机,而且可能出现"假死"(GC停顿、网络中断),所以要用心跳和超时来检测节点存活,但是要注意超时时间的设置,太短会误判,太长会影响可用性。

书里特别强调了一个观点:分布式系统里不能假设任何东西是可靠的,所有的组件都可能出问题,系统要能在部分组件失败的情况下继续工作,也就是要容错。而且分布式系统的故障是部分的、不对称的,不是简单的"要么工作要么不工作",可能出现各种奇怪的中间状态,所以分布式系统的设计和调试都比单机系统复杂得多。

四、第三部分:派生数据

第三部分讲的是派生数据,包括批处理、流处理、数据集成等。这部分讲的是怎么从原始数据派生各种视图,支持不同的查询和分析需求。

1. 批处理

批处理就是对大量数据进行离线计算,比如MapReduce、Spark。批处理的特点是输入是固定的数据集,输出是新的数据集,计算过程中不处理新的输入。

书里讲了MapReduce的原理,以及它的优缺点。MapReduce把计算分成map和reduce两个阶段,通过分布式文件系统(比如HDFS)存储数据,通过排序和分组来连接数据。MapReduce的优势是简单、可扩展、容错,缺点是性能差,因为中间结果要写磁盘,而且不支持迭代计算。

然后讲了各种批处理引擎的演化,比如Tez、Spark、Flink的批处理模式。Spark用内存计算提高了性能,支持迭代计算,适合机器学习和图计算。Flink则是流批一体的引擎。

核心观点是,批处理是数据分析的基础,适合处理历史数据、生成报表、训练模型等场景。批处理的设计原则是:输入是不可变的,输出是确定性的,计算可以重试,容错通过重新计算实现。理解了这些原则,就能更好地设计和优化批处理任务。

2. 流处理

流处理是对持续到来的数据进行实时计算,比如Storm、Spark Streaming、Flink。流处理的特点是数据是持续到来的,计算是持续进行的,结果是实时更新的。

书里讲了流处理的各种概念,比如事件时间、处理时间、水位线(watermark)、窗口、触发器、状态管理、检查点等。这些概念是流处理的核心,也是流处理比批处理复杂的原因。

事件时间是事件实际发生的时间,处理时间是系统处理事件的时间,因为网络延迟和乱序,这两个时间可能不一样。水位线是用来处理乱序事件的机制,表示某个时间点之前的事件都已经到了。窗口是把流分成有限的片段进行计算,比如滚动窗口、滑动窗口、会话窗口。

书里还讲了流处理的用途,比如复杂事件处理(CEP)、流上的聚合、物化视图维护、时间连接、流到流的连接等。以及流处理的容错机制,比如检查点(checkpoint)、保存点(savepoint)、至少一次、恰好一次等语义。

核心观点是,流处理是实时数据处理的基础,适合需要实时响应的场景,比如实时监控、实时推荐、实时风控等。流处理比批处理复杂,因为要处理时间、乱序、状态、容错等问题,但是能提供更低的延迟。选择流处理还是批处理,要根据业务的延迟需求和复杂度来决定。

3. 数据集成和统一日志

这一部分讲了怎么把不同系统的数据集成起来,以及统一日志(unified log)的思想。

常见的数据集成方式有两种:数据仓库(ETL)和统一日志。数据仓库是把各个业务系统的数据定期抽取、转换、加载到数据仓库里,用于分析。统一日志是用一个中心化的日志系统(比如Kafka)作为所有数据的总线,各个系统都从日志系统里消费数据,保持数据同步。

书里特别推崇统一日志的思想,也就是把所有的写入都写到一个日志里,然后各个系统(数据库、缓存、搜索索引、数据仓库等)都从日志里消费数据,保持同步。这样做的好处是数据来源单一,各个系统的数据一致性好,而且可以随时重建任何一个系统的数据,因为日志里有完整的历史。

这个思想也叫CQRS(命令查询职责分离)或者事件溯源(Event Sourcing),核心是把写入和读取分离,写入只写事件日志,读取从各种派生视图里读。这样系统的扩展性和灵活性更好,因为可以随时增加新的读取视图,而不影响写入。

核心观点是,数据集成是大型系统里很重要也很复杂的问题,统一日志是一个很好的思路,能解决很多数据一致性和系统集成的问题。但是它也增加了系统的复杂度,不是所有系统都需要,要根据实际情况选择。

五、这本书给我的启发

最后说说这本书给我的启发。

第一,理解原理比掌握工具更重要。技术工具会不断变化,今天流行的数据库明天可能就过时了,但是背后的基本原理是不变的。理解了数据模型、存储引擎、复制、分区、事务、一致性这些基本原理,不管用什么工具都能快速上手,而且能做出合理的技术选型。

第二,没有银弹,一切都是取舍。分布式系统里没有完美的方案,每个方案都有优缺点,都在一致性、可用性、性能、复杂度之间做取舍。不要盲目追新,也不要觉得某个技术一定比另一个好,要根据业务需求选择最合适的方案。

第三,分布式系统比想象中复杂。很多人觉得分布式系统就是多搭几个节点,其实不是,分布式系统有各种不可靠的因素,网络、时钟、进程都可能出问题,而且会出现各种奇怪的部分失败状态。设计分布式系统要保持敬畏,要考虑各种异常情况,做好容错和降级。

第四,数据是系统的核心。很多工程师关注计算和服务,但是数据才是系统最核心的资产。数据模型的设计、数据的存储、数据的一致性、数据的集成,这些问题比服务的拆分和编排更重要,也更难。要重视数据,理解数据系统的原理。

第五,保持学习,多读经典。这本书出版几年了,但是里面的原理和思想依然适用,而且会一直适用。经典的书值得反复读,每次读都会有新的收获。不要只看最新的技术博客和教程,要多读经典的书,建立扎实的基础。

六、总结

以上就是DDIA这本书的核心观点梳理,内容很多,我尽量提炼了最核心的部分,但是还是有很多细节没有讲到,比如各种算法的具体实现、各种数据库的对比、各种实际案例等。如果对这些内容感兴趣,强烈建议去读原书,绝对值得反复读。

这本书适合有一定后端开发经验的工程师,尤其是做数据库、分布式系统、大数据相关工作的工程师。如果是初学者,可能会觉得有些地方比较难,但是没关系,可以先读一遍建立整体印象,然后在工作中遇到相关问题的时候再回来读,会有更深的理解。

最后想说,作为后端工程师,我们每天都在和数据系统打交道,但是很多时候我们只是在用,并没有深入理解背后的原理。DDIA这本书能帮我们建立对数据系统的系统性理解,让我们不仅知其然,更知其所以然。如果你还没读过,强烈推荐去读一下,相信一定会有收获。

也欢迎读过这本书的朋友在评论区分享自己的读后感和理解,一起交流讨论。