本文以《数据密集型应用系统设计》阅读笔记为起点,按“做架构决策时先问什么”的顺序重新组织。DDIA 第二版已于 2026 年出版,因此这里不复述第一版章节,而是保留跨版本仍然有效的权衡框架。
数据系统选型最容易犯的错误,是先从产品名字开始:“要不要上 Kafka”“该用 MySQL 还是 Elasticsearch”“要不要分库分表”。工具没有脱离约束的优劣,应该先描述工作负载和失败后果,再选择机制。
第一步:把需求写成可判断的约束
至少回答下面这些问题:
| 维度 | 需要量化的内容 |
|---|---|
| 数据规模 | 当前量、年增长、单条大小、保留时间 |
| 访问模式 | 点查、范围查、全文检索、聚合、图遍历 |
| 写入模式 | 单条、批量、追加、更新、删除比例 |
| 时效性 | 同步返回、秒级可见、小时级完成 |
| 正确性 | 能否接受旧读、重复、乱序、短暂缺失 |
| 可用性 | 故障时拒绝、降级还是继续写入 |
| 恢复 | RPO、RTO、能否从源数据重建 |
| 合规 | 审计、删除、地域、加密和访问边界 |
| 团队能力 | 谁运维、怎样升级、是否能演练恢复 |
“数据量很大”“必须强一致”“实时”都不是可执行需求。把它们换成数字、窗口和可接受后果,很多争论会自动消失。
第二步:区分事实源与派生视图
一个系统里不必只有一种存储,但必须知道谁是 system of record。
例如商品系统可以这样划分:
- 关系数据库保存商品和库存事实;
- Elasticsearch 提供全文检索视图;
- Redis 保存可丢失、可重建的热点缓存;
- 数仓保存分析视图;
- 对象存储保存图片和不可变文件。
派生数据的关键不是“永远不丢”,而是:
- 能否从事实源重建;
- 重建需要多久;
- 增量更新丢失时怎样发现;
- 新旧模型如何并行验证;
- 删除和权限变更如何传播。
如果一个搜索索引实际上已经成为唯一数据来源,它就不能继续按“缓存坏了重建”来运维。
第三步:根据访问路径选择数据布局
OLTP 与 OLAP 不要互相勉强
事务系统通常围绕少量记录的低延迟读写设计;分析系统更关心扫描大量列、聚合和压缩。把复杂分析直接压在主业务库上,可能让两种负载互相争抢缓存、I/O 和锁。
应该先问:查询要访问哪些行和列,怎样排序,是否需要最新值,再决定索引、行存或列存,而不是因为“数据多”就自动引入一整套大数据平台。
索引是在写入和读取之间做交换
每个索引都会占用空间并增加写放大。评审索引时记录:
- 它服务哪些真实查询;
- 查询选择性和排序需求;
- 写入额外成本;
- 是否能被另一个复合索引覆盖;
- 过期后谁负责删除。
没有查询所有者的索引,通常会逐渐变成没人敢删的成本。
第四步:把模式演化当成双向兼容问题
滚动发布意味着新旧代码会同时存在。数据编码至少要考虑:
- 新代码能否读取旧数据;
- 旧代码能否忽略新字段;
- 字段删除、重命名和类型变化怎样处理;
- 消息保留时间内是否仍存在旧格式;
- 数据回放时使用哪个 schema 版本。
JSON 可读不代表自动兼容,二进制格式也不天然安全。真正有价值的是明确的 schema、字段演化规则、兼容检查和回放测试。
API、数据库和消息事件的演化节奏不同。尤其不要把内部数据库表结构直接当作长期事件契约,否则一次列名调整会扩散到所有消费者。
第五步:复制解决可用性,也制造一致性问题
选择复制方式时,不要只比较节点数量:
单主复制
- 写入路径容易理解;
- 读副本可以扩展读取和灾备;
- 主从延迟会造成旧读;
- 故障切换要处理未复制写入和脑裂风险。
多主复制
- 适合多地域离线写入或特定协作场景;
- 冲突不再是异常,而是必须定义的业务语义;
- 自动“最后写入获胜”可能静默丢掉用户修改。
无主或 quorum 方案
- 可以容忍部分节点不可用;
- 读写仲裁并不自动等价于线性一致;
- 需要处理版本、修复、时钟和并发写入。
评审重点应该是:故障期间允许什么操作,恢复后如何收敛,用户会观察到什么,而不是把一致性等级当成产品参数勾选。
第六步:分区先看热点和跨分区操作
分区的目标是让数据和负载可以横向分布,但分区键决定了上限。
一个候选分区键至少检查:
- 基数是否足够;
- 写入是否集中到少数值或最新时间段;
- 单个大客户会不会形成热点;
- 主要查询能否带上分区键;
- 事务、唯一约束和 join 是否跨分区;
- 重分片时怎样迁移与限速。
随机散列有利于均匀分布,却可能让范围查询变贵;按时间分区方便归档,却容易把最新写入压到单分区。通常不存在同时满足所有访问模式的分区键,必要时用派生索引服务另一条查询路径。
不要为了未来可能的规模过早分片。单机加副本的关系数据库往往比一个没有恢复演练的分布式集群更可靠。
第七步:一致性由业务不变量决定
先写业务不变量,再选择事务机制。例如:
- 库存不能为负;
- 同一支付单只能结算一次;
- 用户看到“成功”后数据不能消失;
- 权限撤销应在多长时间内生效。
单库事务可以维护局部不变量。跨服务后,可以组合使用唯一键、幂等键、outbox、补偿、对账和状态机,但每种方案都要明确中间状态。
“最终一致”不能成为没有完成条件的借口。至少说明:
- 最终是多久;
- 谁负责重试;
- 重试是否幂等;
- 永久失败进入哪里;
- 怎样发现两边不一致;
- 谁能触发重放或人工修复。
第八步:批处理和流处理是时间边界,不是信仰
批处理适合有边界的数据集、周期性汇总和可重复回放;流处理适合持续输入和低延迟派生结果。很多系统最终是组合:流处理提供快速视图,批处理负责重算、对账或修复。
讨论 “exactly once” 时必须限定边界。消息中间件内部不重复,并不代表外部数据库、邮件或支付动作也只执行一次。端到端通常仍需要:
- 稳定事件 id;
- 幂等消费;
- 原子记录处理结果;
- 可重放输入;
- 对账和人工修复通道。
真正可靠的系统不是从不重复,而是重复和乱序发生时结果仍可解释。
把决策写成可复查的记录
问题与业务不变量:
数据规模和增长:
关键读写路径:
事实源与派生数据:
一致性与时效窗口:
故障期间的行为:
恢复与重建方式:
备选方案及放弃理由:
容量与成本假设:
验证指标:
重新评审条件:
工具会变化,约束也会变化。保留“为什么选择”以及“什么条件下推翻”,比保留一张产品架构图更有长期价值。