当一条查询要跨城市取数,一次写入要同时改变多个节点,数据库面对的就不再只是“怎样存、怎样查”。网络会延迟,消息会重复,机房会失联,两个仍在运行的节点也可能彼此看不见。分布式数据库真正增加的,是对这些不确定性的管理。
我们可以先抓住一条主线:把一个逻辑数据库拆到多个站点之后,系统仍要向使用者提供尽量统一的访问方式;而在内部,它必须明确数据在哪里、谁能修改、跨站点工作怎样结束、故障之后怎样恢复。如果这些问题没有明确答案,多加节点只会把原来的问题放大。
分布式数据库不是“很多数据库放在一起”的简称。它要求多个自治或半自治节点通过协议共同维护数据语义,并在延迟、丢包、节点故障与网络分区存在时,仍给出可解释的查询和事务结果。
分布式数据库把数据放在地理上分开的站点。每个站点有自己的处理器、内存、持久化存储和本地数据库管理能力,站点之间只能通过网络交换消息。某个节点修改了内存里的状态,并不会自动让其他节点知道;只有消息到达、日志被复制或协议完成,远端状态才可能发生变化。
这与单个数据中心里的并行处理有相似之处,却有不同的工程约束。跨地域链路的带宽通常更贵,往返延迟可能达到几十到数百毫秒,链路故障也更常见。一次需要五轮跨地域通信的操作,即使每个节点只计算一毫秒,总耗时也可能主要由网络决定。

只访问发起站点数据的事务是本地事务。它的并发控制、日志和恢复可以在一个节点内闭合。访问远端数据或跨多个节点的事务是全局事务,它会被拆成若干本地子事务,再由协调者统一决定最终结果。
全局事务难在“各节点看到的局部成功不能代表整体成功”。例如,订单节点已经写入订单,库存节点却在扣减前失联。如果两个节点各自提交,系统就留下了无法由单节点恢复算法解释的中间状态。因此,跨节点原子性需要提交协议,而跨节点隔离性需要全局一致的并发规则。
同构系统的节点使用共同的全局模式和相同的数据库软件,能够主动合作处理查询与事务。异构或联邦系统往往来自既有系统整合:各站点可能使用不同模式、类型、查询能力和事务机制,也可能由不同团队管理。
自治程度决定了系统能实施什么协议。统一管理的系统可以要求所有参与者执行同一种提交和复制协议;跨组织的系统通常不会允许外部协调者长期占用本地锁,这时可靠消息、补偿动作和工作流比跨组织 2PC 更现实。
位置透明性让查询不必写出物理节点地址。分片透明性让使用者面对逻辑表,不必手工把多个片段做并集或连接。复制透明性让读取者不必指定“第几个副本”,由系统选择满足一致性和时延要求的副本。
这三层透明性都依赖元数据。目录至少要记录逻辑对象与分片的对应关系、分片所在节点、副本集合、当前主副本或领导者、分片版本以及迁移状态。没有目录,系统无法把一条逻辑请求变成准确的物理计划。
以按用户编号分片的资料表为例。路由器先根据分片函数确定目标分片,再从目录中取得副本位置,然后结合一致性要求、节点健康和距离选择目标。若目录副本落后,请求可能被送到已经迁出该分片的节点。正确做法不是返回错误给业务,而是让旧节点给出重定向,路由器刷新映射后重试。

透明性不应掩盖语义差异。若某个接口允许读取最多落后十秒的副本,另一个接口要求线性一致,调用方必须能够表达这一区别。系统隐藏的是位置和执行细节,不是把所有一致性等级伪装成同一种行为。
水平分片按行划分数据。范围分片保留键的顺序,适合按时间或编号范围扫描;哈希分片通常更均匀,适合按分片键等值定位;轮转分配能平衡行数,却难以根据业务键直接定位。垂直分片按列拆分,通常需要在各片保留主键,才能重建完整记录。
一个正确的分片定义至少要满足完整性、可重建性和尽量少重叠。水平分片的所有片段并起来应得到原关系;垂直分片沿主键连接后应恢复原关系。为了高可用而产生的副本属于复制,不应被误算成分片重叠。

选择分片键时,不要只看分布是否均匀,还要看查询和事务是否能够单片完成。订单按用户编号分片,可让“查看某用户订单”落在一个分片;如果库存按商品编号分片,一次购买多个商品仍可能跨多个库存分片。这里没有万能键,只有与主要工作负载更匹配的选择。
共置是减少网络代价的直接办法。若两个关系经常按同一外键连接,可以使用相同分片函数,把能连接的记录放到同一节点。这样本地连接后只传结果。代价是其他访问路径可能变差,因此要依据最频繁、最昂贵的操作决定共置对象。
数据量均匀不代表负载均匀。按时间范围分片时,最新区间可能承受绝大多数写入;某个头部用户也可能让单一哈希桶成为热点。系统通常把逻辑表拆成远多于物理节点数的虚拟分片。热点出现时,范围片可以继续切分,虚拟分片也能迁移到空闲节点。
迁移必须与目录版本配合。旧路由在短时间内仍会出现,目标节点需要识别当前分片纪元,源节点需要转发或拒绝过期写入,避免两个节点都把自己当成所有者。所谓“自动均衡”并不是后台复制文件这么简单,它改变了写入权和故障恢复边界。
两个副本放在同一台物理机,挡不住机器故障;放在同一机架,挡不住机架供电或交换机故障;放在同一城市,挡不住区域级灾害。副本布局要跨越希望容忍的故障域,同时还要控制跨地域写入延迟。
备用副本也不能集中放置。若节点 A 的所有备用片都放在节点 B,A 故障后 B 会瞬间承接双倍负载。更稳妥的做法是把 A 的不同分片副本分散到多个节点,让故障转移流量也保持均衡。
线性一致描述单个对象的读写:如果写操作已经完成,之后开始的读应看到这个写或更晚的写,并且结果能按真实时间排成一条合法顺序。可串行化描述多个事务的整体效果能否等价于某个串行次序。系统可能对单键提供线性一致,却不保证跨键事务可串行化。
同步复制在提交路径中等待足够副本确认,能缩小数据丢失窗口,但增加写延迟。异步复制允许主副本本地提交后再传播,响应更快,却会产生副本滞后;主副本在日志尚未送达其他节点前永久故障,还可能使已确认写入不可见。

设一个数据项的副本总权重为 ,读仲裁为 ,写仲裁为 。常用安全条件是:
第一个条件保证任意读集合与最近写集合相交;第二个条件保证任意两个写集合相交。读取多个副本时应选择版本号最高的值,必要时顺便修复落后副本。调小读仲裁会让读取更便宜,但必须提高写仲裁。
网络分区时无法同时无条件保证强一致写入和每个分区都可写。多数派一侧继续服务、少数派拒绝写入,是用可用性换一致性;允许两侧都写,则必须接受之后检测和合并冲突的成本。
最朴素的跨源查询是把所有关系搬到发起站点再计算,但这通常浪费网络。选择、投影、局部聚合和可执行的连接应尽量下推到数据所在节点。先把一亿行筛成一万行,再传输结果,往往比更换本地连接算法更有价值。
不同数据源的能力并不相同。完整数据库可能支持 SQL、索引连接和聚合;键值存储可能只能按主键查找;外部服务可能要求某些过滤条件必须出现。优化器需要记录每个源支持的操作、估计返回行数,并把全局计划拆成源端子计划与汇总计划。
若 、、 分别在三个站点,连接可以在查询发起地执行,也可以先把较小关系送到另一个站点,利用该站点索引得到中间结果,再把结果送到第三站点。选择取决于关系大小、选择率、中间结果、节点处理速度和链路成本。
一个简化的跨站点计划代价可以写成:
其中 表示站点间消息往返代价, 是消息轮数, 是单位数据传输代价, 是传输量。大量小请求会放大 ,一次大搬运会放大 ,所以优化器既要减少字节,也要减少往返次数。
假设 在站点 , 在 ,结果要回到 。可以先在 投影连接键,把键集合送到 ; 用这些键筛出真正可能连接的 元组,只把筛选结果送回;最后在 完成连接。

若连接键集合仍然很大,可以发送 Bloom 过滤器。它允许假阳性,也就是少量不能连接的行被多传回来,但不能产生假阴性,因此不会漏掉正确结果。只有当 中实际参与连接的比例足够低,节省的数据量才会超过发送键集合或过滤器的额外成本。
联邦数据库在现有数据源之上增加协调层。每个数据源保留局部模式,协调层提供全局模式,并维护二者之间的映射。查询写在全局模式上,系统把它改写成各站点能执行的局部查询,再把结果转换回全局表示。
全局即视图方法把全局关系定义为若干局部查询结果的组合,查询改写直接,但加入新数据源时常要修改全局定义。局部即视图方法把每个局部源描述成全局关系上的视图,扩展数据源更灵活,查询改写却更复杂。

包装器负责连接数据源、把非关系数据呈现为关系视图、翻译查询并转换结果。它能处理协议和表示差异,却不能自动解决语义冲突。两个系统都叫 length 的列,一个用毫米、一个用英寸;两个字段都叫 status,却有不同状态集合。统一查询前必须确定单位、编码、标识符和空值语义。
数据虚拟化可以不建立完全统一的全局模式,而是让使用者看到多个来源并由平台处理连接与位置。外部表则把远端对象注册到一个数据库中,能否更新取决于数据源能力与映射是否可逆。对于只读分析,还可以周期性把数据汇入仓库,以新鲜度换取查询稳定性。
云平台提供弹性资源和托管数据库,但跨云、跨地域或云地之间的数据传输有时延和费用。设计时要把数据驻留要求、加密与身份边界、出口流量成本、备份恢复责任和供应商故障域一起考虑。把应用搬到云上并不会自动让数据库成为高可用分布式系统。
每个节点的事务管理器执行本地子事务、维护恢复日志并参与并发控制。事务协调者负责启动全局事务、把工作分发到相关节点,并让所有参与者最终同意提交或中止。协调者不是替参与者执行本地写入,而是组织全局结束过程。
协调者先把准备记录强制写入稳定存储,再向所有参与者发送 prepare。参与者检查本地约束、锁、日志和资源是否足以承诺提交。若不能,它记录拒绝并回复中止;若可以,它强制写入 ready 记录和事务更新日志,回复 ready。
ready 是不可随意反悔的承诺。参与者此后不能自行中止,必须保留提交或回滚所需日志,并继续持有冲突锁,直到得知全局决定。这正是 2PC 能保证原子性的原因,也是它可能阻塞其他事务的根源。
只有所有参与者都 ready,协调者才能提交;任何一个拒绝或在准备阶段超时,都应中止。协调者先把 commit 或 abort 决定强制写日志,再通知参与者。参与者记录决定、执行提交或回滚、释放锁并确认。收齐确认后,协调者才可以忘记这次事务。
恢复节点看到 commit 就 redo,看到 abort 就 undo;看到 ready 却没有最终记录时,事务处于 in-doubt,必须向协调者或其他参与者查询。为了不让整个节点的重启被这些事务拖住,ready 日志还可以记录写锁集合。恢复时先重建这些锁,再开放其他事务;只有与不确定事务冲突的操作需要等待。
三阶段提交在准备与最终提交之间增加一个可提交阶段,希望参与者根据更细的状态判断来减少阻塞。某些 3PC 变体在假设没有网络分区、消息延迟有上界时可以避免阻塞;若这些假设不成立,两个网络分区可能作出不一致判断。
因此,3PC 不能被简单描述为“永不阻塞的 2PC”。在真实广域网里,延迟和故障无法被完美区分,安全实现仍要借助多数派或等价的共识机制。
共识让一组节点对日志中的值达成唯一决定。只要多数节点存活并能互相通信,协议可以继续提交日志项。Paxos 和 Raft 的实现细节不同,但共同目标是让所有已提交日志前缀一致,并让旧领导者或延迟消息不能覆盖新任期的决定。
把服务建模成确定性状态机后,只要各副本按同一顺序执行已提交命令,锁表、键值存储或配置服务就能容忍少数副本故障。共识解决的是“决定放在单点会丢失或不可判定”的问题。

一个事务跨越多个共识组时,每个分片内部可以用共识复制日志和锁状态,但不同分片仍需要原子地同意提交或中止。常见做法是保留 2PC,再用共识复制协调者和参与者的决定。这样协调者故障后,新协调者能读取已复制的决定,不必等待原节点恢复。
所以“使用了 Raft”不等于“没有 2PC”。共识保证组内日志和领导权,2PC协调跨组原子结束。若多数派不可达,共识也会停止提交,这是为了避免两个分区各自确认冲突写。
跨组织流程不适合长期持锁时,可以把待发送消息与业务更新放在同一个本地事务中。发送进程反复投递,接收方用唯一消息编号去重,并在同一事务中处理消息和标记完成。网络层可能“至少一次”送达,业务处理依靠持久收件箱做到效果上的恰好一次。
这种模式不提供瞬时的全局原子性。后续步骤失败时,需要应用定义补偿,例如释放预留、退款或转人工处理。它换来的是自治和不阻塞,适合长流程与跨边界协作。
集中锁管理器实现简单,所有锁请求进入一个节点,全局等待图也容易构造;但它可能成为瓶颈和故障点。分布式锁管理让数据所在节点管理本地锁,锁请求不必都经过中心,却让跨节点死锁变得难以发现。
单个站点的等待图无环,不代表全局无死锁。事务甲可能在节点 A 等事务乙,事务乙又在节点 B 等事务甲。只有把各站点等待边合并后,环才出现。中央检测器可以周期性收集等待边,也可以在边变化时增量更新。
网络延迟会让检测器先收到“新增等待边”,后收到“删除旧等待边”,短时间拼出一个现实中不存在的环。若立刻选择牺牲者,就会发生不必要回滚。两阶段锁定下某些假环更难出现,但工程上仍应给等待边附事务纪元和采样时间,并结合二次确认降低误判。

统一加锁顺序可以从结构上消除循环等待。wait-die 一类基于事务年龄的规则允许老事务等待年轻事务,而让年轻事务遇到老事务时回滚;重试必须保留原时间戳,避免一直被当成新事务而饥饿。
租约是带期限的锁。持有者需要续约,失联后租约自动过期,其他节点可以接管。但时钟偏差和延迟消息可能让旧持有者误以为自己仍有权写入。实践中应同时使用单调递增的栅栏令牌:存储端拒绝令牌小于当前纪元的写,让迟到的旧协调者无法破坏新状态。
分布式时间戳可以由本地逻辑时钟与节点编号组合,消息到达时推进逻辑时钟。时间戳排序可在各节点做本地检查,但时钟差距过大会造成事务反复重启。乐观验证也能在各节点执行,不过所有站点必须使用同一验证序,并在提交前完成验证。
各节点独立提供快照隔离也不够:同一事务可能在节点 A 看不到事务甲的更新,却在节点 B 看到事务甲的另一个更新,得到从未同时存在的混合快照。跨节点快照必须使用全局可比较的时间点,并等待该时间点之前的提交状态稳定,或读取一个确定安全的较旧快照。
节点无响应可能是进程崩溃,也可能是链路故障。系统通常无法立即分辨二者,因此不能因为“看不见”就让两个网络分区都取得同一分片的写入权。多数派、任期和租约用可验证规则决定谁有资格继续服务。
故障转移时,新主节点不仅要有数据文件,还要知道已提交到哪里、哪些日志尚未应用、当前分片纪元是什么。它先重放已复制日志,使状态达到安全位置,再接受读写。旧主恢复后必须以跟随者身份重新加入,不能继续使用旧纪元处理请求。
永久故障节点需要从副本目录移除,并在其他故障域补足副本。临时故障节点恢复后要追赶缺失更新。可用日志增量重放;差距过大时,先复制一致性快照,再应用快照之后的日志。重新加入目录的时机必须晚于状态追平,否则请求可能读到旧数据。
多数派读写能容忍落后副本:读取仲裁总能与最近写仲裁相交,并选择较高版本。后台读修复和反熵任务可以逐步修正旧副本。目录本身也应分片和复制,否则它会成为所有请求的中心瓶颈与单点故障。
允许网络分区两侧都写时,单个整数版本号无法区分“先后更新”和“并发更新”。版本向量为每个可能写入节点保存计数。若向量 A 的每一项都不大于 B,A 是 B 的祖先;若 A 在某项更大、B 在另一项更大,两者并发,必须合并或保留冲突版本。
冲突解决取决于业务语义。可交换的“购物车增加商品”可以重放合并;余额变更可以保存操作而不是覆盖最终值;同一文档的两次编辑可能需要人工处理。简单使用最后写入者获胜会丢弃一个分支,而且结果还依赖时钟是否可信。
逐项比较数十亿条记录代价太高。Merkle 树把键空间分桶,叶子保存桶内容摘要,父节点保存子摘要的组合。两个副本先比较根;相同则整棵子树无需传输,不同才向下定位,直到找到差异叶子。
若总数据项为 ,差异项为 ,定位代价可近似写成:
它适合副本反熵与一致性巡检,但只能指出哪里不同,不能替代版本语义、日志顺序或冲突合并规则。
设计分布式数据库时,先把业务承诺写成可检查的条件:哪些读必须看到刚完成的写,哪些列表可以落后几秒;单次写涉及几个分片;区域失联时允许只读、拒绝服务,还是接受冲突写;恢复点目标与恢复时间目标分别是多少。
然后再选择分片键、副本数、故障域、读写仲裁、提交协议和消息模型。反过来从某个流行组件出发,容易得到一套无法解释业务语义的组合。
对最重要的读写各画一条完整路径:入口怎样取得目录,经过几次跨地域往返,在哪个节点取得锁,日志写到哪些副本,什么时候向客户端确认,故障发生在每一步会怎样。路径中每一个“等待远端”都是时延来源,每一个“只写本地”都是潜在丢失窗口。
排查慢查询时,先区分本地计算、排队、锁等待、目录重定向和网络传输。排查写入卡住时,检查参与者处于执行、ready 还是已决定状态;检查协调者日志是否已有决定;检查多数派、任期和时钟,而不是盲目重启所有节点。
应长期记录每个分片的容量与请求偏斜、跨地域字节数和往返次数、副本应用位点与滞后时间、仲裁失败率、领导者切换、in-doubt 事务数、全局死锁与误回滚、目录版本重定向次数、反熵差异量和恢复追赶时间。
这些指标要配合故障演练:切断单条链路、隔离少数派、关闭协调者、让旧主带着延迟消息恢复、让目录短暂落后。只有协议在这些情形下仍给出预期结果,架构图上的“高可用”才算兑现。
一套可交付的分布式数据库方案,应该能用一句话说明每类请求读谁、写谁、等谁确认;还能说明任意一个参与者在任意日志状态下故障后怎样继续。说不清这两件事,系统的正确性就仍依赖运气。