通过理解存储引擎、复制、分区、事务和一致性模型来设计数据系统。当用户提到“数据库选择”、“应该用哪个数据库”、“SQL还是NoSQL”、“复制延迟”、“分区策略”、“一致性与可用性”、“流处理”、“ACID事务”、“最终一致性”、“我的查询在大规模下变慢”或“副本间数据不一致”时使用。在选择数据存储、设计数据管道或调试分布式系统一致性问题时也会触发。涵盖数据模型、批处理/流处理和分布式共识。关于系统设计,请参见system-design。关于弹性,请参见release-it。
设计数据密集型应用框架
构建可靠、可扩展和可维护数据系统的原则性方法。在选择数据库、设计模式、架构分布式系统或推理一致性和容错时应用这些原则。
核心原则
数据比代码更长寿。 应用程序被重写,框架来了又去,但数据会持续数十年——优先考虑数据层的长期正确性、持久性和可演化性。大多数应用是数据密集型的,而非计算密集型的:难题在于数据量、复杂性和变化速率,而明确的一致性/可用性/延迟权衡将健壮的系统与脆弱的系统区分开来。
评分
目标:10/10。 通过以下七个快速诊断行对数据架构进行评分:每行回答“是”并有证据(经过深思熟虑、有文档记录的权衡)得约1.4分,回答“否”或未知得0分。
- 9-10: 每个领域的选择——数据模型、存储引擎、复制、分区、隔离、派生数据、故障处理——都是经过深思熟虑、有文档记录,并与实际的读/写/一致性需求相匹配的;故障转移已测试。
- 5-6: 核心选择已做出,但有两三个诊断行失败——例如,默认隔离级别未知、热键风险未处理或故障转移未测试。
- <=3: 选择由熟悉度驱动,而非需求;忽略了故障模式(复制延迟、写偏斜、热点分区),意外复杂性占主导。
报告当前分数、失败的诊断行以及达到10/10所需的改进。
DDIA框架
推理数据密集型系统的七个领域:
1. 数据模型与查询语言
核心概念: 数据模型塑造了你对问题的思考方式。关系型、文档型和图模型各自施加不同的约束,并支持不同的查询模式。
为什么有效: 选择错误的数据模型会迫使应用程序代码补偿表示不匹配,增加随时间累积的意外复杂性。
关键见解:
- 关系模型擅长多对多关系和即席查询;文档模型擅长一对多关系和局部性;图模型擅长对互联数据的递归遍历
- 写时模式(关系型)及早捕获错误;读时模式(文档型)提供灵活性
- 多语言持久化——为不同的访问模式使用不同的存储——通常是正确的答案
- 对象-关系阻抗失配是真实成本;文档模型为自包含聚合减少了这种失配
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 带嵌套数据的用户资料 | 文档模型用于自包含聚合 | 个人资料、地址和偏好存储在一个MongoDB文档中 |
| 社交网络连接 | 图模型用于关系遍历 | Neo4j Cypher:MATCH (a)-[:FOLLOWS*2]->(b) 用于朋友的朋友 |
| 带连接的财务账本 | 关系模型用于引用完整性 | PostgreSQL外键关联账户、交易、分录 |
在选择关系型、文档型或图模型,或评估读时模式时,请参见 references/data-models.md——包含完整的权衡矩阵和查询语言比较。
2. 存储引擎
核心概念: 存储引擎在读取性能和写入性能之间进行权衡。日志结构引擎(LSM树)优化写入;面向页面的引擎(B树)平衡读取和写入。
关键见解:
- LSM树:仅追加写入、定期压缩、出色的写入吞吐量、较高的读取放大
- B树:原地更新、可预测的读取延迟、页面分裂导致的写入放大
- 写入放大(一次逻辑写入导致多次物理写入)对于写入周期有限的SSD很重要
- 列式存储通过压缩和向量化处理显著改善分析查询
- 内存数据库速度快是因为避免了编码开销,而不是因为避免了磁盘
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 高写入吞吐量 | LSM树引擎 | Cassandra或RocksDB用于每秒10万+写入的时间序列数据 |
| 混合读写OLTP | B树引擎 | PostgreSQL B树索引用于事务性点查询 |
| 分析查询 | 列式存储 | ClickHouse或Parquet用于扫描数十亿行、少量列 |
当工作负载受读/写限制或必须选择索引时,请参见 references/storage-engines.md——包含写/读路径图、压缩策略、列存储和基准驱动的决策过程。
3. 复制
核心概念: 复制将数据副本保存在多台机器上,以实现容错、可扩展性和降低延迟。核心挑战是一致地处理变更。
为什么有效: 每种复制策略都在一致性、可用性和延迟之间进行权衡。明确权衡可以防止仅在负载或故障下出现的微妙异常。
关键见解:
- 单主:简单,可能实现强一致性,但主节点是瓶颈和单点故障
- 多主:跨数据中心更好的写入可用性,但冲突解决复杂
- 无主:通过法定读取/写入实现最高可用性,但需要仔细处理冲突
- 复制延迟导致读后写、单调读和因果性违反
- 同步复制保证持久性但增加延迟;异步复制在故障转移时存在数据丢失风险
- CRDT和最后写入者解决冲突,但正确性保证截然不同
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 读密集型Web应用 | 单主加只读副本 | PostgreSQL主节点 + 只读副本,后端使用pgBouncer |
| 多区域写入 | 多主复制 | CockroachDB或Spanner,带有限制过时性 |
| 购物车可用性 | 无主加合并 | DynamoDB使用最后写入者或应用级购物车合并 |
在选择单主/多主/无主或调试过时读取时,请参见 references/replication.md——包含延迟异常、法定数量计算、冲突解决和CRDT。
4. 分区
核心概念: 分区(分片)将数据分布到多个节点,每个节点处理一个子集,从而实现超越单台机器的水平扩展。
关键见解:
- 键范围分区支持高效的范围扫描,但存在顺序键热点风险
- 哈希分区均匀分布负载,但破坏排序顺序,使范围查询代价高昂
- 本地二级索引需要分散-聚集查询;全局二级索引需要跨分区更新
- 即使使用哈希,当单个键非常流行时也会出现热点(名人问题)
- 再平衡策略:固定分区数、动态分裂或按节点比例
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 时间序列数据 | 按时间+来源的键范围分区 | 按 (sensor_id, date) 分区以避免当前日期的写入热点 |
| 大规模用户数据 | 按用户ID的哈希分区 | Cassandra在 user_id 上的一致性哈希以实现均匀分布 |
| 名人/热键问题 | 带随机后缀的键拆分 | 向热键追加随机数字,将读取分散到10个子分区 |
在分片或处理热键时,请参见 references/partitioning.md——包含再平衡策略、请求路由和本地与全局二级索引的权衡。
5. 事务与一致性
核心概念: 事务提供安全保证(ACID),通过让你在事务范围内假装故障和并发不存在来简化应用程序代码。
为什么有效: 没有事务,每一段应用程序代码都必须处理部分失败、竞争和并发修改。事务将这种复杂性移入数据库,一次正确处理。
关键见解:
- 隔离级别是一个谱系:读未提交、读已提交、快照隔离、可序列化
- 大多数数据库默认使用读已提交或快照隔离——而不是可序列化——因此你必须理解这允许的异常
- 写偏斜:两个事务读取相同数据,做出决定,写入不同记录——没有行锁可以阻止
- 可序列化快照隔离(SSI)乐观地提供完全可序列化:无阻塞,但冲突时中止;两阶段锁定在争用下阻塞和死锁
- 分布式事务(两阶段提交)昂贵且脆弱;应设计单分区操作替代
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 账户余额转账 | 可序列化事务 | BEGIN; UPDATE accounts ... -100 WHERE id=1; UPDATE accounts ... +100 WHERE id=2; COMMIT; |
| 库存预留 | SELECT FOR UPDATE防止写偏斜 | 在减库存前执行 SELECT stock FROM items WHERE id = X FOR UPDATE |
| 跨服务操作 | Saga替代分布式事务 | 扣款、预留库存;失败时执行补偿退款 |
在设置隔离级别或追踪并发错误时,请参见 references/transactions.md——包含每个隔离级别的异常表、写偏斜示例、2PL与SSI以及分布式事务陷阱。
6. 批处理与流处理
核心概念: 批处理批量转换有界数据集;流处理持续转换无界事件流。两者都计算派生数据。
为什么有效: 将记录系统与派生数据(缓存、索引、物化视图)分离,使每个部分可以独立优化,并在需求变化时从源重建。
关键见解:
- MapReduce概念简单但操作笨拙;数据流引擎(Spark、Flink)通过任意DAG将其泛化
- 变更数据捕获(CDC)将数据库写入转化为下游系统可以消费的流
- 流-表对偶性:流是表的变更日志;表是流的物化状态
- 精确一次语义需要幂等操作或事务性输出
- 时间窗口(滚动、跳跃、会话)对于聚合无界流至关重要
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 每日分析管道 | 使用Spark的批处理 | 从S3读取当天事件,聚合,写入数据仓库 |
| 实时欺诈检测 | 使用Flink的流处理 | Kafka支付事件,基于5秒滚动窗口的规则 |
| 同步搜索索引 | 变更数据捕获 | Debezium捕获PostgreSQL WAL,Kafka馈送Elasticsearch |
| 审计追踪/事件重放 | 事件溯源 | 存储 OrderPlaced、OrderShipped 事件;通过重放重建状态 |
在设计管道或从记录系统派生数据时,请参见 references/batch-stream.md——包含数据流引擎、CDC连接、窗口化和精确一次技术。
7. 可靠性与容错
核心概念: 故障是不可避免的;失败不是。可靠系统即使在单个组件失败时也能继续正确运行。为故障设计,而非对抗故障。
关键见解:
- 故障是一个组件偏离规范;失败是整个系统停止——容错防止前者变成后者
- 硬件故障是随机且独立的;软件故障是相关且系统性的(更危险)
- 人为错误是宕机的首要原因——最小化犯错机会,最大化恢复能力
- 超时是基本的故障检测器,但调优困难:太短导致误报,太长延迟恢复
- 安全属性(不发生坏事)必须始终成立;活性(最终发生好事)可能暂时违反
- 拜占庭容错在区块链之外很少需要;假设崩溃-停止或崩溃-恢复
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 服务通信 | 超时+带退避的重试 | retry(max=3, backoff=exponential(base=1s, max=30s)) 带抖动 |
| 领导者选举 | 共识算法(Raft/Paxos) | etcd或ZooKeeper用于分布式锁和领导者选举 |
| 优雅降级 | 断路器 | Resilience4j:在10秒窗口内50%失败后打开断路器 |
在调优超时/重试或添加共识时,请参见 references/fault-tolerance.md——包含故障分类、超时调优数学、Raft/Paxos机制以及安全/活性保证。
常见错误
| 错误 | 失败原因 | 修复 |
|---|---|---|
| 根据流行度选择数据库 | 引擎有根本不同的权衡 | 将存储引擎与实际读写模式匹配 |
| 忽略复制延迟 | 过时读取、幻读、丢失更新 | 实现读后写和单调读保证 |
| 到处使用分布式事务 | 2PC慢且脆弱;协调者是单点故障 | 设计单分区操作;跨服务使用Saga |
| 对所有内容使用哈希分区 | 破坏范围查询能力 | 时间序列使用键范围分区;复合键实现局部性 |
| 假设可序列化隔离 | 默认更弱;写偏斜出现在生产环境 | 检查实际默认值;在需要处使用显式锁定 |
| 混淆批处理和流处理 | 错误工具增加延迟或浪费复杂性 | 将处理模型与数据有界性和延迟需求匹配 |
| 将所有故障视为可恢复 | 损坏和拜占庭故障需要不同处理 | 分类故障;为每类设计恢复策略 |
快速诊断
| 问题 | 如果否 | 行动 |
|---|---|---|
| 你能解释为什么选择这个数据库而不是其他吗? | 选择基于熟悉度而非需求 | 评估数据模型适配度、读写比、一致性需求、扩展路径 |
| 你知道数据库的默认隔离级别吗? | 潜在的并发错误 | 查阅文档;测试写偏斜和幻读 |
| 你的复制策略是明确选择的吗? | 隐含的一致性/持久性假设 | 记录同步与异步、故障转移行为、延迟容忍度 |
| 你的系统能处理热点分区键吗? | 一个流行实体可能拖垮集群 | 为热键添加键拆分或负载削减 |
| 你将记录系统与派生数据分离了吗? | 每次变更都需要迁移所有内容 | 引入CDC或事件溯源以解耦 |
| 超时和重试是调优过的,而非默认的吗? | 级联故障或不必要的延迟 | 测量p99;将超时设置在p99以上、级联阈值以下 |
| 你在生产条件下测试过故障转移吗? | 恢复计划是理论上的 | 运行混沌实验:杀死领导者、分区网络、填满磁盘 |
延伸阅读
如需包含详细图表和研究参考文献的完整论述:
- 《设计数据密集型应用》 作者:Martin Kleppmann
关于作者
Martin Kleppmann 是剑桥大学的分布式系统研究员,曾在LinkedIn和Rapportive担任工程师,以在CRDT和本地优先软件方面的工作而闻名。他的著作《设计数据密集型应用》(2017年)是构建数据系统的工程师的权威参考,因使分布式系统概念易于理解和实用而备受赞誉。






