下面以一条 UPDATE 语句(
UPDATE t1 SET b=10 WHERE a=1;)为例,讲解其在分布式数据库中的执行关键步骤。该语句经过 SQL 引擎的 Parser 和 Optimizer 处理后,进入执行器阶段。
下面详细介绍执行器的关键步骤。
获取全局时间戳
事务开始时,首先需要从中心节点(MC)获取一个全局时间戳。此时间戳作为该事务的快照版本,在整个事务生命周期中用于多版本控制与一致性判断。MC 提供混合逻辑时间戳(物理时间戳 + 逻辑时间戳)。
数据编码
执行 UPDATE 操作需要定位并修改主键、二级索引的记录(包含旧值和新值)。所有数据均已编码为 Key-Value 形式。以下是此语句涉及的关键 Key 示例:

0000271180000001:对应 a=1 主键的值0000271208000000180000001:对应二级索引 b=1 的旧值(即原记录 a=1, b=1)0000271208000000A80000001:对应二级索引 b=1 的新值(a=1,b=10)计算引擎路由缓存
生成 Key 后,需要确定每个 Key 的存储节点位置,这依赖于路由功能。在 TDSQL Boundless 中,元数据管理模块 MC 掌握全局路由分布情况。这里的路由本质上是 Key 区间到节点的映射关系。
路由动态特性
相较于分库分表架构(只需确定分片对应的 DB 节点位置),弹性扩缩容的自适应调度架构具有更复杂的路由特性:
路由可能动态变化。
数据区间动态调整。
每个 Key Range 的大小不确定。
两级路由缓存机制
考虑到执行引擎不可能对每条 SQL 的每个 Key 都向 MC 发起 RPC 请求获取最新路由(这将显著增加系统开销),我们在 SQL 引擎内部实现了路由缓存机制。该缓存采用两级设计:
分桶缓存:路由缓存被分片存储,每个桶独立缓存所需路由信息。
全局缓存:进程内唯一的 global 级别缓存。
两级缓存的工作机制如下:
执行 SQL 时首先从所在分桶获取路由
当出现缓存未命中或路由过期时,转向 global 缓存查询。
若 global 缓存也未命中或路由过期,则通过 RPC 向 MC 获取最新路由。
这种分层设计既减少了大锁竞争的影响,又降低了 RPC 调用开销。
路由层级分析
Region 粒度路由
是数据读写操作中最常用的路径。当执行 put 或 get 等数据读写操作时,系统通过 key 值定位数据所属的数据分片。
核心数据结构是 key 值到 region 的映射关系。
采用 upper bound 方法确定 key 所属的 region,进一步定位到具体节点。
Replication Group 粒度路由
主要用于事务提交和回滚时的重试操作。
当事务提交或回滚时,系统不再关注具体 key 值访问,而是基于已有的事务上下文,通过获取相应的 Replication Group 路由进行重试操作。
Node 粒度路由

Node 粒度路由,进程全局一份,基本只读。
在系统弹性扩缩容场景下,最常见的调度操作大部分集中在 region 层面(包括数据分片的切分与合并)和 replication group 层面(如主节点切换,即将整个replication group 及其下属 region 从 node1 迁移至 node2)。Node 粒度相对稳定,仅在面临资源瓶颈需要扩容时才会增加节点。
RPC 异常处理
在确定 KV 数据目标节点后,系统通过 RPC 将操作指令发送至存储引擎。以 UPDATE 操作为例,需要执行以下 RPC 流程:
1. 通过PK GetRecord 获得旧版本完整 Record
发起 Get 操作获取旧行数据,即在主键 PK(例如字段 A)上获取完整的旧版本记录。Get RPC 消息的结构可划分为以下四个部分:

第一部分(黄色背景):Replication Group (RG) 元信息,用于校验当前读写操作是否采用了最新的路由。例如存储层接收 Get 请求时,会核验请求中携带的 Region Version 是否与本地最新版本匹配。若版本不匹配,则表明使用了过期的路由访问了错误的节点。此时,SQLEngine 需要从管控节点 MC 获取最新路由,并将 Get 请求重试到正确的新位置。
第二部分(绿色背景):事务快照信息。
第三部分(红色背景):Get 操作的目标 Key 值。
第四部分(蓝色背景):Schema Object ID 及 Version 信息,该部分与 DML/DDL 并发控制相关。
GetRecord RPC 异常处理

网络错误:
错误场景:SQL 节点作为 RPC 发起方,能够区分连接失败或超时,但无法明确具体原因。
可能场景
请求传输异常导致延迟,存储层未收到 GET 请求。
存储层收到请求但回包延迟超过 RPC 时限。
存储层收到请求但执行操作耗时过长,未能在时限内返回响应。
处理方式:由于 GET RECORD 属于单点只读操作且具有幂等性,可直接重试。
路由错误:
处理方式:刷新路由缓存,将 GET RPC 发送至 Key 所在的最新正确路由。
特性:该 RPC 具有幂等性,重试不会引发数据错误。
其他错误类型
DML 与 DDL 并发报错
锁超时
内部报错
2. UK SCAN 做唯一性检查
由于二级索引 b 是唯一索引,写入新数据 b=10 前需检测是否已存在 b=10 的记录。可通过 SCAN 进行唯一性检查。
SCAN RPC 与 GET 类似,但不具备完全幂等性。在进行网络错误或路由错误重试时,需关注执行进度与偏移量,避免重复查询或遗漏数据。

ScanRecord 断点续传示例:

1. 初始计划:扫描 A 到 Z 范围
2. 首次请求:发送至 Region1,返回 A 到 G 范围数据
3. 区域分裂:Region1 发生分裂,不再包含 G 到 Z 的全部数据
4. 错误处理
Region1 返回路由变化错误
SQL 层更新路由
发现 G 到 Z 范围分布于 Region1 和 Region2
5. 续传机制
拆分请求:G 到 K 发送至 Region1,K 到 Z 发送至 Region2
确保扫描既不重复也不遗漏
3. 删除二级索引

4. 写入 PK 新行,覆盖旧行
写入主键的新记录。
5. 写入 UK 新行
写入二级索引新记录。对于主键来说,由于 UPDATE SQL 没有修改 a=1 的值,只要 PUT 新记录,即可覆盖旧行。
在二级索引的处理过程中,需要进行 DELETE 操作。写操作 RPC 与读操作相比,存在以下关键差异:
网络错误的处理策略
当写操作遇到网络错误时,系统必须断开与客户端的连接。
基于数据正确性保障考虑,避免重试数据包与原始数据包乱序到达存储层,从而导致数据异常。尽管 TCP 协议本身能保证数据有序传输,但在网络封装或协程框架实现层面,无法完全排除因协程调度延迟等因素,导致重试数据包比首次发送的数据包更早写入 Socket 的可能性。
安全保障机制
处理方式:在检测到写 RPC 网络报错时主动断开客户端连接。
效果:写入 RPC 连接中断后,整个事务不会提交,从而避免错误数据的产生。

6. 提交
当读写操作全部完成后,系统会提交事务:
事务提交的 RPC 消息相比读写操作更为简单,只需提交事务 ID 即可。
若事务提交过程中遇到网络错误,同样会断开连接。
状态不确定性处理:类似于单机 MySQL 执行过程中进程被误杀的场景,客户端会收到 "lost connection" 错误,无法确定事务是否执行成功,需要客户确认先前事务的执行结果。

DML 和 DDL 并发
在分布式数据库环境中,必须严格控制 DML(数据操作语言)与 DDL(数据定义语言)的并发执行,核心原因在于:DDL 操作(如增删列、改类型)会使表结构经历一个短暂的“中间状态”。如果此时 DML 操作基于旧的表结构写入数据,那么当 DDL 完成后,无论是新结构还是旧结构,都将无法正确解析这部分“穿越”过来的数据,导致数据损坏。
因此,任何一个健壮的数据库系统都必须建立一套机制,确保 DML 和 DDL 操作的严格隔离。
单节点环境:元数据锁 (Metadata Lock, MDL)
在单个数据库节点内,通常使用元数据锁(MDL)来协调。MDL 是一种比行锁粒度更大的锁,它保护的是表的“元数据”,也就是表的结构定义。
场景一:DDL 运行时,DML 等待
1. Session1 (DDL) 先开始,并且当前没有其他活动事务,它成功获取了表的 X lock,并开始修改元数据或拷贝数据。
2. 此时 Session2 (DML) 尝试对该表进行读写,它需要获取 S lock。
3. 由于 S 锁与 X 锁互斥,Session2 必须进入 waiting 状态,直到 Session1 的 DDL 操作完全结束并释放 X 锁。

场景二:DML 运行时,DDL 等待
1. Session2 (DML) 先开始一个事务,执行读写操作,并成功获取了表的 S lock。
2. 此时 Session1 (DDL) 尝试修改表结构,它需要获取 X lock。
3. 由于 X 锁与 S 锁互斥,Session1 必须进入 waiting 状态,直到 Session2 提交或回滚事务,释放所有 S 锁。

分布式环境:Write Fence 机制杜绝 DML 写入旧格式数据(DDL 优先级更高)
分布式协调流程:

1. DML 启动 (Node2):
一个客户端连接到 Node2,发起一个 DML 事务。
Node2 执行
Open table 操作,从全局元数据中心获取了表的定义(我们称之为 Schema V1)并缓存在本地内存中,准备后续的数据读写。2. DDL 并发执行 (Node1):
几乎在同一时间,另一个客户端连接到 Node1,发起一个 DDL 命令(例如
ALTER TABLE ADD COLUMN ...)。Node1 成功执行了 DDL,将表的结构更新为 Schema V2,并同步更新了全局元数据中心。
3. 冲突发生 (Node2):
Node2 上的 DML 事务处理完毕,准备通过 Put/Get RPC 调用将数据写入底层存储。
关键冲突点:Node2 准备写入的数据是按照它所缓存的旧结构 Schema V1 进行编码的。但此时,整个系统(由元数据中心定义)已经认为表的结构是 Schema V2。
表版本号 (Table Version) 与写入围栏 (Write Fence)
为了解决 DML 与 DDL 的并发冲突,分布式系统引入了以“表版本号”(
schema_version)为核心的并发控制机制。它就像一个 “写入围栏”,能有效阻止 DML 操作向已过时的表结构中写入数据,从而保障数据一致性。其核心设计思想如下:
DDL 的职责:当执行修改表结构的操作时(如新增列、索引等),DDL 事务将该表的
schema_version 推进到新版本,并持久化到数据字典(Data Dictionary)中。DML 的职责:在执行
Open table 操作时,不仅需要获取表的分区信息和索引信息,还需要同时获取对应的 schema_version。以下是一个具体示例:
1. Node1 (DML): 客户端连接
Node1 执行 DML,在 Open table 阶段缓存了表的旧版本号 t.schema_version=1。2. Node2 (DDL):与此同时,
Node2 上成功执行了 DDL。此操作将表的版本号从 1 推进到 3。3. Node3 (DML):
Open table -> 获取 t.schema_version=3 -> 发起 Put/Get RPC(data, version=3) -> Region Leader 校验 3 == 3 -> OK。4. Node1 (DML): 发起
Put/Get RPC(data, version=1) -> Region Leader 校验 1 != 3 -> 拒绝请求,返回 Raise Error。因此,问题进一步转化为:DML 如何判断当前持有的
schema version是否已过期? 
DML 如何感知表结构变化
最初,我们考虑让 DML 客户端在每次读写操作前,主动向“数据字典”(Data Dictionary)查询最新的表结构版本。但这个方案存在两个致命缺陷:
性能开销 (Performance Overhead): 每次
put 或 get 操作都需要一次额外的 RPC 来访问数据字典,这会极大地增加系统延迟,降低吞吐量。原子性问题 (Atomicity Problem): 在“查询最新版本”和“执行写入 RPC”之间存在一个时间窗口。如果在这个窗口期内,表结构再次被 DDL 修改,系统依然无法保证数据的一致性。
为了解决上述问题,我们采用了将版本校验责任下沉至存储层的方案。该机制的核心是在存储节点上引入一个名为 “写入围栏”(Write Fence) 的结构。
Write Fence 的本质是持久化存储了每个表/索引对象(schema_obj_id)与其当前最新的版本号(schema_obj_version)的映射关系,形成一个 <schema_obj_id, schema_obj_version> 的元组。当一个 DDL 事务执行时,它必须原子地完成两件事:
1. 更新数据字典: 将最新的
schema_version(例如从 1 更新到 2)持久化到数据字典中。2. 更新写入围栏: 将最新的版本元组(如
<t1: <10001, 2>>)持久化到所有相关数据分片(Region)所在的存储节点(Node 1)的 Write Fence 中。
DML 执行与校验流程如下所示:
1. 请求准备: DML 客户端在
Open table 时获取到表结构版本,例如 t.schema_version=2。2. RPC 携带版本: 在发起
Put/Get RPC 时,请求体(如 GetRecordRequest)中会携带 schema_obj_id 和 schema_obj_version(例如 schema_obj_id = 10001, schema_obj_version = 2)。3. 存储层原子校验: 存储节点在收到请求后,会将 RPC 中的版本元组
<10001, 2> 与其本地 Write Fence 中存储的元组进行比对。版本匹配: 如果版本一致,说明 Put 请求获取的表结构是最新的,操作被允许执行(
OK)。
版本不匹配: 如果版本不一致(例如请求携带
version=1,而 Write Fence 中是 version=2),说明 DML 持有的表结构已过时。存储层会拒绝该请求,并向上层抛出错误,触发整个 DML 或事务失败并重试。