如何设计分布式数据库
从 MiniDB 单机撑不住开始,长出复制、选主、分片、跨分片查询、分布式事务、再平衡和运维边界
阅读定位: 这页讲“数据库为什么从一个节点变成多个节点”,不替代通用分布式系统、消息队列、缓存或大数据系统。我们只沿着 MiniDB 的数据库主线往外扩:单机故障、读压力、容量、热点、跨分片事务和跨节点查询。
一句话原则: 分布式数据库不是“把数据库部署成集群”这么简单。它是在节点会挂、网络会断、数据会分散、请求会跨节点的现实里,继续维持可用性、正确性和可解释性。
单机 MiniDB 已经有存储、索引、事务、MVCC 和优化器。但只有一个节点时,磁盘坏、进程挂、机房断电,数据库就不可用。
| 单机已经解决 | 单机仍然失败 | 下一步需要什么 |
| 事务和 WAL 能处理进程崩溃 | 整台机器不可用时,用户仍然不能读写 | 至少还有一个副本能接管 |
| Buffer Pool 和索引提升性能 | 读压力集中在一台机器 | 读副本分担查询 |
| 优化器能选择单机计划 | 数据太大,单机磁盘和 CPU 不够 | 把数据分到多个节点 |
阶段验收: 你能说清:分布式数据库的第一动机通常不是“更高级”,而是单机的故障域、容量和吞吐边界到了。
最直觉的扩展是一个 primary 接收写入,把 WAL 或变更流复制给 replica。读请求可以走副本。
Primary:
commit tx17 at LSN=502
send log records to Replica A / Replica B
Replica:
receive LSN=502
replay log
expose read at replayed_lsn=502
| 解决了什么 | 马上撞上的问题 | 设计边界 |
| 读压力可以分散到副本 | 副本可能落后主库 | 读请求要知道能否接受旧数据 |
| 主库挂了还有副本数据 | 谁来当新主 | 需要选主和故障切换 |
| 复制基于 WAL 比复制整页更清晰 | 网络中断时日志积压 | 需要复制位点和落后监控 |
这一步的教训: 主从复制不是“多一份数据”就完了。副本读到的是哪个 LSN,决定了它能回答多新的问题。
如果 primary 挂了,两个副本都觉得自己该升主,就会出现双主写入。分布式数据库最怕的不是慢,而是两个节点各自接受写入,历史分叉。
| 失败场景 | 后果 | 需要的机制 |
| 网络分区,旧主还活着但被部分节点看不见 | 旧主和新主同时接受写入 | 多数派选主 / fencing |
| 落后副本被提升为主 | 已经提交的数据可能丢失 | 只允许足够新的副本参选 |
| 客户端继续写旧主 | 写入进入错误历史 | leader epoch / lease / 拒绝旧任期写入 |
Leader election rule:
only node with majority votes can become leader
each write carries leader_epoch
nodes reject writes from stale epoch
阶段验收: 人为制造网络分区时,系统宁可让少数派不可写,也不能出现两个主同时写。
异步复制提交快,但主库刚 commit、日志还没到副本时主库挂了,可能丢失已返回成功的写入。于是 MiniDB 集群要决定 commit 需要多少副本确认。
| 复制策略 | 优点 | 失败代价 | 适合场景 |
| 异步复制 | 写入延迟低 | 主库挂时可能丢已提交数据 | 读扩展、可容忍少量丢失 |
| 半同步复制 | 至少一个副本收到后返回 | 仍可能有边界窗口 | 折中方案 |
| 多数派提交 | 提交有多数派证据 | 写入延迟更高,少数派不可写 | 强一致、高价值数据 |
关键取舍: 复制不是免费安全。你是在“提交延迟、可用性、丢失窗口”之间做选择。
复制让每个节点都有一份全量数据,不能降低单节点容量压力。表太大时,必须把不同 key 范围或 hash 槽放到不同 shard。
| 分片方式 | 解决什么 | 马上暴露的问题 |
| Hash Sharding | 写入更均匀,热点少一些 | 范围查询要跨很多 shard |
| Range Sharding | 范围查询和顺序扫描友好 | 热点范围可能压垮单 shard |
| Tenant / User Sharding | 同一用户数据尽量共址 | 大租户会变成超级热点 |
| 手工分库分表 | 实现简单可控 | 迁移、扩容、跨库查询都很痛 |
阶段验收: 你能解释为什么“复制三份”和“分成三片”解决的是不同问题:前者偏高可用和读扩展,后者偏容量和写扩展。
有了分片,客户端不能再直接连某台数据库。它要根据分片元数据把 SQL 路由到对应 shard。
Shard Map:
user_id hash slot 0..1023 -> shard A
user_id hash slot 1024..2047 -> shard B
user_id hash slot 2048..3071 -> shard C
| 请求类型 | 路由方式 | 风险 |
| 带 shard key 的点查 | 直接路由到单 shard | 最好处理 |
| 不带 shard key 的查询 | 广播到所有 shard 再合并 | 慢且容易放大压力 |
| 跨 shard Join | 拉取多 shard 数据做 Join | 网络和中间结果可能爆炸 |
| 分片元数据变更 | 路由表需要更新 | 旧路由可能写错 shard |
设计提醒: 分布式数据库的性能,很大程度取决于业务查询是否带着正确的分片键。
单机事务只需要协调一个 WAL 和一个事务管理器。跨分片转账时,两个 shard 都要成功,否则钱可能凭空消失或增加。
transfer 100 from user A to user B
Shard 1:
debit A
Shard 7:
credit B
failure:
Shard 1 committed, Shard 7 failed
| 方案 | 能解决什么 | 代价 |
| 2PC | 参与者先 prepare,再统一 commit / abort | 协调者故障会阻塞,延迟高 |
| 共识复制事务日志 | 事务决议有多数派证据 | 实现复杂,写路径更长 |
| 业务 Saga / 补偿 | 把长事务拆成可补偿步骤 | 不是强原子,需要业务接受中间状态 |
| 避免跨分片事务 | 把强一致边界放在一个 shard 内 | 需要从业务建模阶段设计分片键 |
阶段验收: 你能区分:哪些操作必须跨分片强一致,哪些可以通过异步补偿或最终一致处理。
单机优化器只比较本地 IO 和 CPU。分布式优化器还要估算网络传输、数据所在节点、是否能下推过滤、是否需要重分布。
| 优化动作 | 为什么重要 | 失败场景 |
| Filter Pushdown | 先在 shard 内过滤,再传少量结果 | 把全量数据拉到协调节点再过滤 |
| Partial Aggregation | 每个 shard 先局部聚合 | 把所有明细行传回来再 sum |
| Colocated Join | 同一 shard key 的数据本地 Join | 跨节点 shuffle 大量数据 |
| Distributed Join | 必要时重分布数据 | 中间结果和网络成本爆炸 |
关键变化: 分布式查询优化器要控制的不只是中间结果行数,还有数据移动距离。
新加节点后,部分 shard 要迁移过去。迁移期间仍然有读写,系统必须避免双写、漏写、读到半迁移状态。
| 迁移阶段 | 风险 | 设计处理 |
| 复制历史数据 | 复制期间源 shard 还在写 | 记录迁移起点 LSN,追增量日志 |
| 追平增量 | 永远追不上热点 shard | 限流、短暂停写或分批切换 |
| 切换路由 | 客户端有旧路由缓存 | 路由版本号和过期拒绝 |
| 清理旧副本 | 过早删除导致回滚困难 | 确认无旧路由写入后再清理 |
阶段验收: 分片迁移时持续写入,最终新旧 shard 的数据必须能用 LSN 或校验和证明一致。
Hash 分片看起来均匀,但真实业务常有超级用户、热门租户、秒杀商品、月底结账等热点。平均负载健康,不代表系统健康。
| 热点类型 | 症状 | 应对 |
| 单 key 热点 | 一个 shard CPU / lock wait 飙升 | 拆 key、缓存、队列化或业务限流 |
| 范围热点 | 新写入都落在最新时间范围 | 时间范围加 hash 后缀,或预分裂 range |
| 大租户热点 | 一个租户占用大量资源 | 大租户独立 shard 或资源隔离 |
| 跨分片热点查询 | 广播查询拖慢所有 shard | 物化汇总、查询改写、限制无 shard key 查询 |
不要误读: 分片不是把热点自动摊平。分片键选错,热点会被更隐蔽地放大。
| 阶段 | 遇到的问题 | 长出的设计 | 真正吸收点 |
| 单机 | 机器挂了全站停 | 复制 | 副本缩小故障影响 |
| 复制 | 副本读旧、主库故障 | LSN、选主、故障切换 | 复制必须有进度和任期 |
| 异步复制 | 提交成功也可能丢 | 半同步 / 多数派提交 | 安全性会换来延迟和可用性代价 |
| 全量复制 | 容量仍在单节点 | 分片 | 复制和分片解决不同问题 |
| 分片 | 请求不知道去哪 | 路由层和 shard map | 分片键是数据库和业务的共同边界 |
| 跨分片修改 | 一边成功一边失败 | 2PC / Saga / 避免跨分片 | 跨节点原子性很贵 |
| 跨节点查询 | 网络搬运数据太多 | 下推、局部聚合、分布式计划 | 优化器要控制数据移动 |
| 练习 | 故意制造什么失败 | 通过标准 |
| 异步复制延迟 | 主库 commit 后立刻读副本 | 能暴露副本 replayed_lsn,并决定是否等待 |
| 主库故障切换 | primary 挂掉,副本选主 | 只出现一个新主,旧主恢复后不能继续旧任期写入 |
| 少数派网络分区 | 3 节点切成 1 + 2 | 少数派不可写,多数派继续服务 |
| 分片路由 | 带 shard key 和不带 shard key 查询对比 | 能看出单 shard 查询和广播查询成本差异 |
| 跨分片转账 | 一个 shard prepare 后另一个 shard 失败 | 能进入统一 commit / abort 或业务补偿路径 |
| 分片迁移 | 迁移期间持续写入 | 新旧 shard 通过 LSN 或校验证明一致 |
| 热点 key | 大量请求集中同一用户或租户 | 能从指标定位热点 shard 和热点 key |
如何设计查询优化器
如果你还没理解单机计划选择、统计信息和 Join 顺序,先回到
如何设计查询优化器。
如何设计数据库高可用、读写分离与故障切换
如果你关注的不是 DBMS 内部复制和选主,而是生产业务系统怎样处理主库故障、从库延迟、旧主隔离、连接池重连、切后容量和回切演练,继续看
如何设计数据库高可用、读写分离与故障切换。
如何设计数据库分库分表与数据再平衡
如果你关心的是业务系统如何从单库单表迁到分库分表,怎样设计 shard key、路由版本、历史复制、增量追平、热点租户和扩容再平衡,继续看
如何设计数据库分库分表与数据再平衡。
DDIA
如果你想把复制、分区、事务、批处理和流处理放进更大的数据系统框架,继续看
《DDIA》。
总结: 分布式数据库不是 CAP 名词表,而是单机失败边界一步步扩出来的系统:机器会挂,于是复制;主库会挂,于是选主;异步复制会丢提交,于是有同步程度取舍;单机容量不够,于是分片;分片后请求要路由;跨分片事务很贵;跨节点查询要控制数据移动;扩容迁移和热点治理又把复杂度带回日常运维。