☰
分布式数据库系统复习:从分片、事务到故障排查
2026/9/26 5:50:39 网站建设 项目流程

简介:这份《分布式数据库系统》复习文档是面向数据库课程期末考试、考研复习或自学巩固的整理资料,覆盖填空、简答与论述三大题型,可帮助读者快速厘清知识框架。包体为单个 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 这类多数派协议则取中间态,要求多数派节点确认才算提交,可以容忍少于一半的节点故障,这也是不少分布式数据库系统默认选择多数派写的原因。

写入一条数据的路径,在多数派协议下大致是这样的:

  1. 客户端连接协调节点,发出写请求
  2. 协调节点按分片键定位到目标分片
  3. 请求被转发给该分片的 leader 副本
  4. leader 写入本地日志并并行发给 follower
  5. 多数派确认日志落盘后,leader 返回客户端成功
  6. 后台继续把日志应用到状态机,并异步同步给滞后节点

这个路径决定了两个常见调优点。一是读一致性:如果允许从 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 的标准实现,它对业务侵入小,但缺点也很明显:事务内所有操作必须在一个全局锁窗口里完成,跨机网络延迟会让锁持有时间放大很多倍。于是就有了 TCC(Try / 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 --threads=16 --time=300 \ --mysql-host=127.0.0.1 --mysql-user=root \ --db-driver=mysql --tables=10 --table-size=1000000 \ 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% 后搬迁和数据均衡的余量就不够了。这套估算方法帮我避开了很多"上线两个月磁盘就打满"的翻车事故。

我现在的习惯是任何新集群上线前,先做一次故障演练,把每个故障的处理顺序打印出来贴在值班文档里,而不是等到告警响了再现场推理。这套方法不是什么高深技巧,但它能把你从一次真实的故障中学到的东西固化下来,变成团队的下一次从容应对。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询