mcpField
D
ddia-principles

ddia-principles

github.com/luoling8192数据与数据库17237 次安装31 周前更新
代码仓库

关于 ddia-principles

《Designing Data-Intensive Applications》的精编学习指南,面向学习数据系统设计与运维的读者。内容按可靠性、存储、分布式一致性及派生数据等主题组织。

主要功能

  1. 01概述可靠性与可扩展性原则
  2. 02比较数据模型和存储引擎
  3. 03讲解复制与分区
  4. 04梳理事务隔离级别与异常
  5. 05涵盖分布式一致性与共识

适用场景

  1. 01复习数据系统设计概念
  2. 02比较存储或复制方案
  3. 03学习一致性与事务取舍

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/luoling8192/ai-coding-principles --skill ddia-principles

安装后在 Claude Code 中输入/ddia-principles确认已加载。

详细介绍

数据密集型应用系统设计——精要指南

来源:Martin Kleppmann,《数据密集型应用系统设计》 核心论点:现代应用的核心挑战在于数据,而非计算。


第一部分:数据系统基础

第 1 章:可靠性、可扩展性、可维护性
三大支柱
支柱定义关键指标
可靠性系统即使发生故障也能正常工作故障 ≠ 失效;容忍故障,防止失效
可扩展性系统能够平稳应对负载增长用百分位数衡量:p50、p95、p99、p999
可维护性系统易于运维、理解和演进可运维性 + 简洁性 + 可演进性
故障类别
  • 硬件:随机、相互独立(磁盘、RAM、电源)。通过冗余(RAID、双电源)缓解。
  • 软件:同时影响所有节点的系统性错误(闰秒错误)。通过进程隔离、监控、混沌工程缓解。
  • 人为因素:宕机的首要原因(配置错误)。通过良好的抽象、沙箱、金丝雀部署和快速回滚缓解。
可扩展性模式
  • 纵向扩展(scale-up):使用更大的机器。简单,但存在上限。
  • 横向扩展(scale-out):增加机器(无共享架构)。复杂,但扩展空间不受限。
  • 弹性扩展:检测到负载后自动扩展。适用于不可预测的工作负载。

Twitter 扇出案例研究:每秒 4.6k 次写入,但每秒 300k 次读取。解决方案:为大多数用户预先计算时间线(写入扇出);对名人用户在读取时合并。

性能:使用百分位数,而非平均值
  • p50 = 中位数。p99 = 尾部延迟,对用户体验至关重要。
  • Amazon:延迟增加 100ms = 收入损失 1%。
  • 尾部延迟放大:一次缓慢的后端调用会拖慢整个并行请求。

第 2 章:数据模型与查询语言
模型选择指南
模型最适合弱点
关系型结构化数据、复杂联接、ACID 事务模式僵化,与 OOP 存在阻抗失配
文档型层次化数据、灵活模式、数据局部性联接能力弱,多对多关系处理不佳
图高度关联的数据、可变深度遍历工具链不够成熟,更难分区
模式策略
  • 写入时模式(关系型):在写入时强制执行结构。可及早发现错误,但需要承担迁移成本。
  • 读取时模式(文档型):在读取时解释结构。灵活,但验证责任落在应用上。
规范化与反规范化
  • 规范化:单一事实来源、更新一致,但需要联接。
  • 反规范化:读取更快,但存在不一致风险和更新异常。

趋势:各种模型正在融合——PostgreSQL 支持 JSON,MongoDB 增加了联接。应根据访问模式选择,而非意识形态。


第 3 章:存储与检索
存储引擎对比
特性B-TreeLSM-Tree
写入吞吐量较低(原地更新 + WAL)较高(顺序追加)
读取延迟更可预测可能需要检查多个 SSTable
写放大较高较低
空间效率可能产生碎片压缩效果更好
事务支持更简单(锁定树节点)更复杂
使用方PostgreSQL、MySQL、OracleLevelDB、RocksDB、Cassandra
OLTP 与 OLAP
方面OLTPOLAP
访问方式随机访问,少量记录顺序扫描,数百万行
用户终端用户分析师
数据当前状态历史事件
规模GB–TBTB–PB
优化目标低延迟高吞吐量
数据仓库
  • ETL:从 OLTP 提取 → 转换 → 加载到仓库。
  • 星型模式:中央事实表(事件)+ 维度表(属性)。事实表可包含 100 多列,规模可达 PB 级。
  • 列式存储:每列单独存储。当查询只涉及少数列时,可大幅节省 I/O。支持位图编码、游程压缩和向量化处理。

第 4 章:编码与演进
格式比较
格式大小(示例)架构演进跨语言
JSON81 字节隐式手动极佳
Thrift59 字节必需字段标签良好
Protobuf33 字节必需字段标签极佳
Avro32 字节必需名称匹配良好
兼容性规则
  • 向后兼容:新代码读取旧数据。(始终必需)
  • 向前兼容:旧代码读取新数据。(滚动升级时必需)
  • 规则:只添加或删除带有默认值的字段。切勿重复使用已删除的字段标签。
数据流模式
  1. 通过数据库:滚动部署期间,多个代码版本并存。数据的生命周期长于代码。
  2. 通过服务(REST/RPC):先更新服务器,再更新客户端。请求需保持向后兼容,响应需保持向前兼容。
  3. 通过异步消息:解耦生产者和消费者。支持独立演进版本。

避免:特定于语言的序列化(Java Serializable、Python pickle)——会导致供应商锁定,并带来安全风险。


第二部分:分布式数据

第 5 章:复制
复制模型
模型写入冲突用例
单主一个节点无最常见(PostgreSQL、MySQL)
多主多个节点必须解决多数据中心、离线客户端
无主任意节点必须解决Cassandra、Riak、Voldemort
同步与异步复制
  • 同步:持久可靠,但副本故障时会阻塞。
  • 异步:速度快,但主节点故障时有数据丢失风险。
  • 半同步:一个副本同步,其余副本异步。实用的折中方案。
复制延迟问题与解决方案
问题症状解决方案
读己之写用户看不到自己写入的数据读取用户自己的数据时,从主节点读取
单调读数据时间倒退将用户固定到同一个副本
一致前缀读因果顺序被破坏将存在因果关系的数据写入同一分区
冲突解决
  • 最后写入胜出(LWW):简单但会丢失数据。仅当键不可变时才安全。
  • 合并:对值取并集、进行拼接,或使用 CRDT 数据结构。
  • 应用层处理:返回所有版本(“兄弟版本”),由应用决定。
  • 版本向量:跟踪每个副本的因果依赖关系。
仲裁:w + r > n
  • w = 写入确认数,r = 读取查询数,n = 副本总数。
  • 宽松仲裁:分区期间接受写入到非归属节点(提示移交)。提高可用性,但会降低一致性。

第 6 章:分区(分片)
分区策略
策略优点缺点
键范围范围查询高效顺序键可能导致热点
哈希分布均匀不支持范围查询
复合对第一部分进行哈希,其余部分排序更复杂,但兼具两者优点
二级索引分区
  • 本地(基于文档):每个分区为自己的数据创建索引。写入简单,读取需要分散收集。
  • 全局(基于术语):按术语对索引分区。读取高效,写入需要更新多个分区。
重新平衡
  • 固定分区数:分区数多于节点数。节点变更时重新分配。(Riak、Elasticsearch)
  • 动态:根据大小进行拆分/合并。(HBase、RethinkDB)
  • 与节点数成比例:每个节点的分区数固定。(Cassandra)
请求路由
  • 轮询到任意节点(节点会在需要时转发)
  • 路由层(感知分区的代理)
  • 感知客户端(客户端了解分区映射)
  • ZooKeeper:权威的分区 → 节点映射。HBase、Kafka 使用此方式。

第 7 章:事务
隔离级别(从弱到强)
级别防止允许实现方式
Read Committed脏读、脏写不可重复读、丢失更新行锁 + 旧值副本
Snapshot Isolation+ 不可重复读写偏斜、幻读MVCC(多版本)
Serializable所有异常无2PL、串行执行或 SSI
并发异常
异常描述示例
脏读读取未提交的数据读取只写了一半的转账记录
脏写覆盖未提交的数据两位买家都“赢得”同一件商品
丢失更新读-修改-写竞争两次并发的计数器递增
写偏斜基于过期读数做决策两位医生都停止值班
幻读新行改变查询结果会议室被重复预订
可串行化实现
  1. 串行执行:单线程、内存中执行。速度快,但吞吐量有限。(VoltDB、Redis)
  2. 两阶段锁(2PL):共享锁 / 排他锁一直持有到提交。保证性强,但速度慢,容易发生死锁。
  3. SSI(Serializable Snapshot Isolation):乐观方式——自由执行,在提交时检测冲突。最适合读密集型工作负载。(PostgreSQL 9.1+)

第 8 章:分布式系统中的问题
三种不可靠性

网络:异步分组网络——不保证送达,也不保证时序。无法区分崩溃和网络延迟。超时是唯一的故障检测器,但不存在正确的超时值。

时钟:

  • 墙上时钟:可能向后跳(NTP 校正)。绝不要用来排列事件顺序。
  • 单调时钟:可安全测量经过的时间,但不能用于跨节点比较。
  • 石英时钟漂移:约 200ppm → 每 30 秒误差 6ms。

进程:GC 暂停、VM 挂起、缺页——线程可能毫无预警地停止。暂停的节点不知道时间已经过去。

核心原则
  • 故障 ≠ 失效:针对部分故障进行设计。某些节点正常工作,而其他节点可能无法工作。
  • 真相由多数派决定:单个节点无法独自确定系统状态。由法定人数投票决定。
  • 隔离令牌:单调递增的令牌可防止僵尸进程破坏状态。
  • 安全性与活性:安全性(坏事永不发生)必须始终成立。活性(好事最终发生)可能需要满足条件。

第 9 章:一致性与共识
一致性模型(从强到弱)
模型保证代价
Linearizability行为如同只有一个副本,所有操作都是原子的高延迟,分区期间可用性降低
Causal consistency遵循因果顺序性能更好,可容忍分区
Eventual consistency副本最终会收敛性能最佳,保证最弱
线性一致性的使用场景
  • Leader 选举 / 分布式锁
  • 唯一性约束(用户名、文件名)
  • 跨通道协调(消息队列 + 存储)
共识算法
  • 2PC:基于协调者,协调者故障时会阻塞。实用但脆弱。
  • Paxos/Raft/Zab:基于 Epoch 的 Leader 选举 + 法定人数投票。不会阻塞。etcd、ZooKeeper、Consul 使用此方式。
  • FLP 不可能性:在存在崩溃的纯异步系统中,共识不可能实现。实际算法使用超时。
全序广播 ≡ 共识 ≡ 线性一致的 CAS

这三个问题在数学上等价。解决其中一个,就能解决全部。

ZooKeeper / etcd 模式
  • 用于协调的小型共识集群(3–5 个节点)。
  • 线性一致的原子 CAS 操作。
  • 通过会话心跳检测故障。
  • 应用场景:领导者选举、分区分配、分布式锁、服务发现。

第三部分:派生数据

第 10 章:批处理
Unix 哲学 → MapReduce
  • 每个程序都专注做好一件事。
  • 一个程序的输出 = 另一个程序的输入。
  • 输入不可变,处理结果确定。
MapReduce 流水线

输入 → Mapper(提取键值对)→ 排序/分区 → Reducer(按键聚合)→ 输出

分布式联接策略
联接类型适用场景实现方式
排序归并两个输入都很大按联接键排序,在 Reducer 中归并
广播哈希一个输入较小(可放入 RAM)将较小的一侧加载为哈希表
分区哈希两个输入采用相同方式分区对每个分区执行哈希联接
超越 MapReduce:数据流引擎(Spark、Flink、Tez)
  • 将整个工作流视为单个作业。
  • 流水线处理中间结果(避免将数据完整物化到 HDFS)。
  • 尽可能将数据保留在内存中。
  • 跟踪计算血缘以便在故障后恢复(RDD)。
  • 支持迭代算法(通过 Pregel/BSP 模型进行图处理)。

第 11 章:流处理
消息代理模型
模型投递方式顺序保证重放使用场景
AMQP/JMS逐条确认,确认后删除不保证顺序否任务队列、异步 RPC
基于日志(Kafka)基于偏移量,消息保留分区内有序是事件流、CDC
变更数据捕获(CDC)

将数据库变更提取为事件流 → 让派生系统(搜索索引、缓存、数据仓库)保持同步。事实来源仍在数据库中;派生视图是消费者。

事件溯源

将状态建模为只追加的业务事件序列(而非数据库操作)。事件是不可变的事实。当前状态 = 对事件历史进行折叠计算。

流联接
联接输入 A输入 B状态
流-流事件事件按时间窗口缓冲
流-表事件数据库快照(通过 CDC)本地物化表
表-表CDC 流CDC 流派生物化视图
时间与窗口
窗口描述
滚动窗口固定大小、不重叠(例如,每 1 分钟)
跳跃窗口固定大小、彼此重叠(例如,每 30 秒计算一个 1 分钟窗口)
滑动窗口时间间隔不超过阈值的所有事件
会话窗口按活动间隔分组(例如,30 分钟无活动)

事件时间 ≠ 处理时间。 为确保正确性,始终使用事件时间。使用水位线或更正发布来处理迟到事件。

处理保证
  • 微批处理(Spark Streaming):延迟约 1 秒,采用原子化的小批次。
  • 检查点(Flink):通过消息屏障定期创建快照。
  • 幂等性:使用消息偏移量或唯一 ID 去重。
  • 端到端恰好一次需要幂等输出和去重。

第 12 章:数据系统的未来
数据集成模式

没有任何单一数据库可以处理所有事情。使用事件日志作为集成骨干:

  1. 所有写入都经过权威事件日志。
  2. 派生系统(索引、缓存、机器学习模型)消费该日志。
  3. 使用确定性、幂等函数在各层之间进行转换。
数据库解耦

分离不同职责:

  • 记录系统:记录权威写入。
  • 派生系统:索引、缓存、物化视图消费变更流。
  • 支持渐进式迁移——让新旧系统并行运行。
端到端恰好一次

底层保证(TCP、数据库事务)无法确保应用正确性。需要:

  • 操作标识符:在应用层使用基于 UUID 的去重。
  • 幂等操作:执行一次或多次,效果相同。
  • 唯一约束强制执行:通过分区流处理实现。
异步约束强制执行

不使用分布式事务,而是:

  1. 按约束字段将请求路由到分区日志。
  2. 流处理器对竞争请求排序。
  3. 拒绝违反约束的请求,并通过输出流通知客户端。
  4. 有些应用可以容忍暂时违反约束,并通过补偿事务处理。
可审计性
  • 将数据视为不可变事件日志,以便重建和验证。
  • 实施加密审计追踪(Merkle 树)。
  • 定期测试备份恢复和数据重建。

决策框架:快速参考

如何选择数据模型
Many-to-many relationships?     → Relational or Graph
Hierarchical / nested data?     → Document
Highly connected data?          → Graph
Flexible / evolving schema?     → Document (schema-on-read)
Strong consistency required?    → Relational (ACID)
如何选择存储引擎
Write-heavy workload?           → LSM-tree (RocksDB, Cassandra)
Read-heavy, predictable?        → B-tree (PostgreSQL, MySQL)
Analytical queries?             → Column store (ClickHouse, Redshift)
Full-text search?               → Inverted index (Elasticsearch)
如何选择复制策略
Single datacenter?              → Single-leader
Multi-datacenter?               → Multi-leader
Offline-first clients?          → Multi-leader or Leaderless
Maximum availability?           → Leaderless with sloppy quorum
Strong consistency?             → Single-leader with sync replication
如何选择隔离级别
Read-only analytics?            → Snapshot isolation
General OLTP?                   → Read committed (default in most DBs)
Financial / critical?           → Serializable (prefer SSI over 2PL)
High write contention?          → Serial execution (if data fits in RAM)
如何选择批处理或流处理
Historical data reprocessing?   → Batch (Spark, Flink batch mode)
Real-time derived views?        → Stream (Kafka + Flink/Spark Streaming)
Both needed?                    → Unified engine (Flink) over Lambda architecture

评论

支持 Markdown