
简介这份《分布式数据库系统》复习文档是面向数据库课程期末考试、考研复习或自学巩固的整理资料覆盖填空、简答与论述三大题型可帮助读者快速厘清知识框架。包体为单个 doc 文档大小 38KB属于纯文本型资料方便按关键词检索、打印或导入笔记软件。资料核心内容包括分布式数据库的同构/异构分类全局控制集中型、分散型与可变型架构水平分片、垂直分片与混合分片方法集中式、分割式、复制式和混合式数据分布策略同时整理了数据库管理系统四大功能模块、分布透明性三层次、DATAID-D 设计流程、分布式查询优化准则、事务 ACID 特性、并发控制封锁算法及死锁检测方法等高频考点。文档还对数据分片规则、分布式事务一般结构、数据分配策略等简答论述题给出了要点式答案适合考前突击或作为教学备课提纲使用。目前已有 656 人浏览学习是同类复习资料中较为凝练的一份。1. 分布式数据库系统为什么先理解复习再动手选型凌晨两点收到告警订单库单节点磁盘打满慢查询把连接池占满你翻出各种分布式数据库资料发现概念都眼熟但面对故障时依然不知道先查哪里。这个场景我见过太多次。所谓复习分布式数据库系统不是把分片、复制、事务、一致性这些名词再背一遍而是把它们的因果链理顺形成一套从问题到手段的决策顺序。这篇笔记就按这条主线来写数据怎么放、写入怎么同步、跨节点事务怎么做、性能怎么调、故障怎么排。它适合 DBA、后端开发也适合准备技术答辩的工程师读完后你能对分布式数据库系统的全貌有可落地的把握。2. 分片与复制把数据先放对位置再谈性能分布式数据库系统与单机数据库最大的区别是数据不再天然在一块磁盘上而是被切成多份分布在多个节点。如果分片策略不对后面所有的高可用和分布式事务都是空中楼阁。所以第一步不是研究一致性协议而是想清楚每一行数据该去哪台机器。2.1 分片键的三个候选hash、range、list 的取舍分片键的选择是所有问题的起点。最常见的三种策略是 hash 分片、range 分片和 list 分片。hash 分片对写入分散最友好按键值散列后基本能做到数据均匀但如果你的业务经常按范围查询hash 就会把一次范围查询变成跨分片聚合range 分片让范围查询很舒服但 Time 类型的单调递增字段会让新写入全部落在同一个分片上热点非常危险list 分片适合按地域、租户这类有限枚举值分片灵活但需要维护映射关系数据倾斜要靠枚举设计去扛。用订单表举例我一般会写成下面这样的示意 DDL重点不是语法而是分片键的取舍思路-- 这只是一段示意 DDL用来表达分片键思路不是某个产品的官方语法 CREATE TABLE orders ( order_id VARCHAR(64) NOT NULL, customer_id VARCHAR(64) NOT NULL, status TINYINT NOT NULL DEFAULT 0, amount DECIMAL(10,2) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (order_id) ); -- 常见做法把“谁查得最多”放进分片键 -- 按 customer_id 做 hash 分片而不是按 order_id ALTER TABLE orders SHARD BY HASH(customer_id);这段逻辑里最关键的是按谁分片。按 customer_id 分片后同一个用户的订单一定落在同一个分片上针对用户维度的查询都能路由到单分片执行如果你按 order_id 分片单用户订单会被打散到几十个分片查用户订单列表时就必然做跨分片聚合。代价是订单表的主键不能再是单一 order_id因为你按 customer_id 分片后跨分片无法保证全局唯一主键常见做法是把主键改成(customer_id, order_id)的组合或者用全局发号器生成 id 后仍把 customer_id 作为路由键。分片方式与业务场景的匹配可以简单对照这张表分片方式数据分布范围查询扩容主要风险hash 分片均匀需跨分片搬迁后需要重新散列范围查询性能差range 分片按值连续单分片友好容易切分时间类热点list 分片按枚举分组组内友好调整映射即可枚举倾斜2.2 副本数与写路径同步复制、异步复制、多数派为什么不能混用分片解决的是数据分布副本解决的是数据安全与可用性。一个分片通常有多个副本最常见的架构是一个主副本负责写多个从副本负责读或备份。从副本的同步方式直接决定了主备切换时的丢失窗口同步复制下主库要等从库落盘才返回成功性能损失明显异步复制延迟小但主库宕机且从库尚未追上主库时会丢数据Raft 这类多数派协议则取中间态要求多数派节点确认才算提交可以容忍少于一半的节点故障这也是不少分布式数据库系统默认选择多数派写的原因。写入一条数据的路径在多数派协议下大致是这样的客户端连接协调节点发出写请求协调节点按分片键定位到目标分片请求被转发给该分片的 leader 副本leader 写入本地日志并并行发给 follower多数派确认日志落盘后leader 返回客户端成功后台继续把日志应用到状态机并异步同步给滞后节点这个路径决定了两个常见调优点。一是读一致性如果允许从 follower 读你很可能读到旧数据因为 follower 的日志应用可能落后常见做法是让对一致性敏感的业务走 leader 读或者启用会话级线性一致读但后者每次读都要跟 leader 确认RTT 更高。二是超时和并发写路径经过协调节点再到分片 leader链路变长客户端侧的超时要比单机库设置得更宽松否则网络抖动一两次就把事务挡在外面。2.3 扩容时的数据搬迁一致性哈希与虚拟节点分片键定好不是一劳永逸数据量增长后必然要加节点。如果最初用的是简单 hash 后取模扩容时几乎全部数据都要搬迁这在生产环境是不可接受的。所以多数分布式数据库系统会引入一致性哈希或类似机制把数据分成比较小的区间每个区间对应一组副本节点加入后只需要把部分区间迁到新节点而不是全量重算。为了减轻单个节点故障时哈希值映射剧烈变化的影响虚拟节点是一个很常见的补充手段一个物理节点持有多个虚拟区间数据分布更均匀搬迁粒度也更细。这里有个容易被忽略的成本在线扩容会触发大量副本搬迁带宽、磁盘 IO 和 CPU 都被占用线上流量反而可能变慢。我一般建议把搬迁并发数调小、限速执行并且先加副本、等副本追平后再迁移 leader而不是让系统一次性把 leader 和副本全部搬完。新加入的节点初期 leader 数量是 0它只是接收副本数据当大多数副本在新节点上就绪后再把 leader 切过去。这个顺序能明显降低扩容窗口内的可用性风险。3. 分布式事务从两阶段提交到最终一致的选型路线分片解决了容量和并发但也带来了新问题原本在一个库里的多张表被分散到不同分片后跨分片更新就需要分布式事务。这一章是复习分布式数据库系统时最容易绕晕的地方因为名词很多2PC、XA、TCC、SAGA、本地消息表。先别急着背定义关键在于理解每种方案的阻塞点和适用场景。3.1 两阶段提交为什么会卡住协调者的两难两阶段提交2PC是最直接的分布式事务方案。第一阶段协调者问所有参与者能不能提交每个参与者写日志、加锁、准备好然后回复 Yes第二阶段协调者根据所有回复决定 Commit 或 Abort通知所有参与者执行。听起来很完美但问题出在第一阶段之后、第二阶段完成之前参与者的资源是被锁住的而协调者如果在这个窗口宕机所有参与者都无法自行决定是提交还是回滚只能一直等。这个场景我用一个协调者的 Python 伪代码来说明# 两阶段提交协调者的核心流程伪代码 class TwoPhaseCommitCoordinator: def __init__(self, participants): self.participants participants # 多个分片上的事务参与者 self.prepared {} # 记录每个参与者的准备结果 def run(self, transaction): # 阶段一prepare for p in self.participants: result p.prepare(transaction) # 参与者加锁、写 undo/redo 日志 self.prepared[p.id] result if not result: self.abort(transaction) # 有参与者失败直接中止 return # 阶段二commit for p in self.participants: p.commit(transaction) # 释放锁真正生效这段伪代码的坑在于 prepare 返回成功后参与者已经持有锁并写好了日志但还没有收到 commit 指令。如果协调者进程挂掉参与者只能持续持有锁直到协调者恢复并给出最终决定。所以生产环境里的两阶段提交不能只写这套流程还必须给事务加状态持久化协调者把每个参与者的 prepare 结果写入独立事务日志启动时扫描日志恢复未完成的事务状态。即便如此2PC 的锁持有时间仍然偏长高并发下容易把数据库连接和锁资源耗尽这也是很多互联网业务不直接使用 XA 的原因。3.2 从 XA 到 TCC业务层分布式事务的三个关键动作数据库厂商提供的 XA 事务是 2PC 的标准实现它对业务侵入小但缺点也很明显事务内所有操作必须在一个全局锁窗口里完成跨机网络延迟会让锁持有时间放大很多倍。于是就有了 TCCTry / Confirm / Cancel这种业务层方案。TCC 把一次事务拆成两个阶段Try 阶段做业务检查并预留资源Confirm 阶段真正执行提交Cancel 阶段做补偿回滚。它不依赖数据库的 XA 协议而是靠业务代码保证最终正确性。TCC 最常踩的是空回滚和悬挂问题。空回滚发生在 Try 还没成功执行Cancel 就先到了这时候如果不做判断直接回滚会导致后续 Try 真正执行时数据状态错乱。我一般会在事务表或日志表里记录每个事务的执行状态Cancel 先查状态如果 Try 不存在或者未成功就只记录 Cancel 成功不动业务数据。下面是一段示意代码// TCC 中空回滚的处理先查幂等记录再判断 Try 是否真的成功 public boolean cancel(String txId, String userId, BigDecimal amount) { // 1. 先查幂等记录避免同一个取消请求被重复执行 if (idempotentRepository.exists(txId)) { return true; } // 2. 查 Try 阶段的执行日志 TryLog tryLog tryLogRepository.getByTxId(txId); if (tryLog null || !tryLog.isSuccess()) { // Try 没执行成功Cancel 不能把余额扣成负数 idempotentRepository.mark(txId); return true; } // 3. 正常补偿把预留时扣掉的余额加回去 accountRepository.increaseBalance(userId, amount); idempotentRepository.mark(txId); return true; }这段代码里的 idempotentRepository 和 tryLogRepository 就是防悬挂的关键它们分别记录幂等状态和 Try 执行结果。没有这两个表Cancel 和 Try 的乱序到达就会造成重复补偿或者漏补偿。注意这只是一个片段真实 TCC 还需要处理 Confirm 阶段的失败重试以及 Try 和 Cancel 并行时的加锁顺序。TCC 的优点是性能好、锁控制灵活难点是业务侵入性高每个操作都要写三套逻辑。如果你的团队能把订单、库存、账户这类核心业务抽象成一套标准化接口TCC 是值得投入的如果只是为了某一个低频管理功能引入分布式事务会更建议选择 SAGA 或本地消息表这类最终一致方案。3.3 SAGA 与消息表用最终一致性换取可用性SAGA 和 TCC 的差别在于节奏。TCC 是预留-确认-取消SAGA 是正向操作 反向补偿。第一次提交订单正向操作包括创建订单、扣减库存、扣减余额当扣减库存失败时反向补偿操作依次是加回库存、取消订单。SAGA 不需要预留资源锁释放得更快但中间状态是暂时不一致的别人可能在提交过程中看到订单已创建、但库存还没扣减成功所以 SAGA 更适合那些最终对账能兜底的业务不适合任何一个瞬时状态都不能出错的资金强校验场景。本地消息表是一个更朴素但非常可靠的最终一致方案。你在业务库建一张消息表业务操作和消息写入在同一个本地事务里完成然后通过后台任务把消息表记录发给下游分片下游消费成功后标记完成。这套方案不依赖分布式事务协调器也不容易被协调者单点问题卡住是很多订单系统的兜底选择。它的代价是消息表可能成为瓶颈消费延迟也会导致数据可见性延迟。方案一致性强度锁持有时间业务侵入适用场景XA / 2PC强一致长低金融核心、短事务TCC较强一致中高电商交易链路SAGA最终一致短中长流程、异步链路本地消息表最终一致短中跨团队、跨系统通知真实业务里很少只用一个方案。我见过很多企业是主链路用 TCC订单状态推送用本地消息表对账用 SAGA 反查混合使用比单押一种方案更常见。4. 调优与巡检让集群在真实写入下保持稳定的参数清单分片和事务方案定下来后性能调优考验的是你对集群运行时参数的熟悉度。分布式数据库系统的性能瓶颈往往不在磁盘而在协调节点、连接池和跨分片聚合所以调优的第一原则是分清瓶颈在哪一层是客户端连接数太多是协调节点 CPU 打满还是分片 leader 的磁盘 IO 已经饱和。4.1 连接池与线程模型把并发均匀压到分片上连接池是第一个要看的参数。单机数据库的经验是连接池越大越好但在分布式数据库里每个连接背后对应协调节点的一路线程资源连接数一旦超过协调节点的 CPU 核心数线程切换就会开始消耗 CPU。我一般的习惯是连接池上限设置为协调节点 CPU 核心数的 2 到 4 倍并且把读流量和写流量分开连接池。读流量可以走只读副本或者独立路由避免一个大查询把写事务的连接池全部占满。分片层的连接池也一样协调节点会为每个跨分片事务建立到多个分片的连接连接数会被事务数量放大。所以客户端连接数调小不一定代表性能下降反而能降低协调节点和分片之间的连接风暴风险。调完连接池后要观察两个指标协调节点线程池的活跃线程数和等待队列长度。如果活跃线程经常打满说明并发超出了集群处理能力而不是连接数不够。4.2 三个必调参数事务超时、慢查询阈值、副本确认级别即使再好的架构线上也难免出现慢事务和故障这时超时阈值就是止损的底线。下面是一组我常用的参数示意不同产品关键字有差异但思路是通用的# 常见参数示意不是某个产品的固定语法 SET GLOBAL slow_query_threshold_ms 200; SET GLOBAL txn_timeout_sec 30; SET GLOBAL replica_read_consistency linear;slow_query_threshold_ms 设成 200 毫秒是为了让那些平时快、压力下突然超过 200 毫秒的查询快速暴露。不要直接设成 100 或 50分布式环境正常网络 RTT 就可能消耗几十毫秒阈值太严会把所有查询都变成慢查询日志真正的异常反而被淹没。txn_timeout_sec 设成 30 秒是针对两阶段提交和 TCC 等长事务的兜底超过 30 秒还没有拿到所有参与者确认这个事务大概率是碰到锁等待或者协调者异常了与其继续阻塞不如快速失败让上层重试。replica_read_consistency 你可以理解为读一致性的级别linear 表示线性一致读读请求需要跟 leader 确认如果业务能接受秒级延迟改成 follower 读性能会明显提升但前提是你清楚这个取舍。参数设完之后还要定期看集群自带的监控视图重点关注分片之间的数据量是否倾斜。同一个哈希分片算法跑久了某些数据分布仍然可能出现偏差常见表现是某几个分片的磁盘使用率远高于其他分片。这类倾斜不能靠重启解决只能通过调整分片键或拆分热点分片来处理。4.3 EXPLAIN ANALYZE把慢查询拆到分片级别慢查询排查时最忌讳一上来就猜。先把 SQL 执行计划打出来看数据访问方式是否真的利用了分片键。我现在遇到跨分片聚合的慢查询会先按 customer_id 维度验证是不是可以改成单分片查询再考虑优化聚合逻辑。-- 看一条查询会访问哪些分片、哪些步骤产生跨分片数据交换 EXPLAIN ANALYZE SELECT customer_id, COUNT(*) AS order_cnt FROM orders WHERE customer_id C10001 GROUP BY customer_id;这条 SQL 因为条件里带 customer_id理论上只路由到单个分片。EXPLAIN 结果里会显示访问的是哪个分片、是否走了局部索引、是否需要对多个分片的结果做合并。如果不带 customer_id只写SELECT COUNT(*) FROM orders执行计划就会显示扫描全部分片再在协调节点做全局聚合。这里要注意的是全局聚合消耗的是协调节点的内存和 CPU而不是分片节点的资源所以同一个慢查询在单机上可能只影响自己在分布式数据库里却可能拖累所有业务的协调请求。5. 五个容易翻车的故障场景现象、原因与处理顺序这一章是血泪经验每一条我都希望你在上线前就看过。分布式数据库系统的故障通常不是单点故障而是多个组件同时异常后的连锁反应处理顺序错了会让恢复时间翻倍。5.1 热点分片订单按时间分片被写爆现象每天零点后订单量猛增某个分片的磁盘 IO 和 CPU 瞬间打满其他分片却很空闲查询延迟从几十毫秒涨到几秒最终连接池被等锁事务占满。原因分片键用了 created_at 做 range 分片新增订单永远落在最新时间区间的分片上。时间是一个单调递增的自然键它天然会把所有新写入集中到一条线上这是分布式数据库里最经典的热点陷阱。解决把分片键改成不会递增的业务维度比如 customer_id 或 tenant_id 做 hash 分片如果业务必须按时间范围查询则采用组合策略例如用(tenant_id, created_at)作为分片键让时间只在租户内部递增。已经在跑的系统如果无法改分片键常见的补救办法是提前预建多个子分片并设置合并规则把未来几天的数据分散到不同分片上但这是治标不治本。5.2 分布式事务协调者重启后参与者锁不释放现象一个跨 5 个分片的事务执行到一半协调者进程异常重启业务侧看到大量行锁等待重启协调者后 5 分钟内还是持续报错因为参与者一直没收到 commit 或 rollback 指令。原因协调者没有把 prepare 结果做持久化重启后丢失了事务状态参与者持有的锁没有权限自行释放。解决给协调者的事务管理器增加状态表每个 prepare 结果都先落盘再回复参与者启动恢复流程时扫描未完成事务对已经全部 prepare 成功的事务执行 commit对部分失败的事务执行 rollback。参与者侧也要设置事务超时超过超时时间未收到最终指令的本地事务自动中止防止锁被无限持有。5.3 网络分区恢复后出现主键冲突现象机房之间的网络中断一段时间恢复后出现大量主键冲突告警甚至发现部分数据丢失。原因网络分区期间少数派节点上的旧 leader 如果还在接收写入就会产生脑裂数据多数派协议本身能选出一个新 leader但如果少数派节点没有正确拒绝写入旧 leader 上的数据就会成为脏数据。解决检查所有节点的选举与 quorum 配置确认写入必须要求多数派确认旧 leader 在分区恢复后不能直接恢复为 leader必须回滚未提交的日志并重新追赶新 leader 数据。处理顺序一定是先让少数派停止服务再追赶复制最后再恢复读写顺序反过来会把脏数据重新灌入集群。5.4 一条慢查询拖垮所有业务现象某个报表团队跑了一个跨全分片的大查询结果其他业务的小查询也响应变慢连接数飙升。原因大查询在协调节点做全局聚合占用了大量线程和内存协调节点的资源是共享的大查询把线程池排满后普通查询只能排队等待。解决把数据分析类查询拆到独立队列或独立路由组并设置查询内存上限和扫描行数上限协调节点检测到超过阈值的跨分片大查询自动降级为异步执行或者直接拒绝。对业务查询确保 EXPLAIN 里能看到分片键过滤条件避免全分片扫描。5.5 扩容搬迁导致性能不升反降现象给集群新增了节点结果迁移任务启动后业务延迟反而升高甚至出现短暂无主窗口。原因数据搬迁吃满带宽和磁盘 IO同时搬迁调度器把大量 leader 也一并迁到新节点导致新计算的流程都集中在新节点上它还没追上副本就被打上 leader 标记性能雪崩。解决扩容操作梳理成三步走先加节点并只搬迁副本等副本追平再批量切 leader每一步之间观察集群的平均延迟和队列长度。搬迁并发数建议调到默认值的一半以下并限制搬迁使用的带宽上限如果业务不允许性能受损只在低峰期执行扩容这些都是可以把风险压到最低的习惯。6. 验证分布式数据库系统的最后一公里压测、故障演练与容量估算很多人复习到调优参数就结束了但真正决定一个分布式数据库系统是否可靠的是上线前的验证。把压测和故障演练做扎实比看任何监控大盘都有效因为故障只有在它真正发生的时候才最有说服力。6.1 压测要分两层先打单分片再打全集群先压单分片是为了找单节点的性能基线再压全集群是为了看协调节点和跨分片聚合的扩展性。如果直接压全集群卡在协调节点时你很难定位是分片的问题还是路由层的问题。压测时可以用 sysbench 这类工具打个底关键在于观察 TP99 和线程数之间的拐点# sysbench 压测是一类常见做法先小并发跑出基线 sysbench --threads16 --time300 \ --mysql-host127.0.0.1 --mysql-userroot \ --db-drivermysql --tables10 --table-size1000000 \ oltp_read_write run线程数从 16、32、64 逐步往上加如果 TP99 在 64 线程时突然抖动到几秒这个点就是当前配置的瓶颈上限。此时不要盲目降线程而要看是协调节点 CPU 打满还是分片节点的磁盘 IO 打满。压测结束后保留一份报告后续每次调参都对比同一组压测数据落地效果才说得清楚。6.2 故障演练的三个标准动作第一直接杀掉分片 leader 节点观察集群自动选主的时间和业务失败数正常情况应该在几十秒内完成切换。第二模拟协调节点网络分区观察客户端是否出现逻辑错误而不是无限等待。第三在扩容搬迁过程中压入流量确认搬迁限速参数是否真的生效。做完这三个动作再把数据写为一份故障处理顺序表故障发生时照着顺序操作人就不会慌。6.3 一个容量估算公式我一般用这个公式做新集群容量规划节点数不小于 数据总量 × 副本数 除以 单节点有效容量 乘以 水位线。比如数据总量是 20TB副本数是 3单节点有效容量 4TB水位线 0.7那么最少节点数约为 22 节点。水位线 0.7 的意思是在磁盘使用率到达 70% 前要考虑扩容超过 70% 后搬迁和数据均衡的余量就不够了。这套估算方法帮我避开了很多上线两个月磁盘就打满的翻车事故。我现在的习惯是任何新集群上线前先做一次故障演练把每个故障的处理顺序打印出来贴在值班文档里而不是等到告警响了再现场推理。这套方法不是什么高深技巧但它能把你从一次真实的故障中学到的东西固化下来变成团队的下一次从容应对。希望帮到你。本文还有配套的精品资源点击获取