TDSQL分布式事务操作
图中引入了一个核心组件:Proxy(即图中的中间层),它充当了事务管理器(TM)的角色。
TDSQL 分布式事务执行流程详解
我们将图中的数字编号(1-8)展开,还原整个执行链路:
1. 开启事务 (Step 1)
- 动作:客户端发送
begin指令给 Proxy。 - 细节:Proxy 在内存中创建一个名为Gtxn (Global Transaction)的数据结构。这个结构体是该事务在分布式环境下的“身份证”,用于记录后续涉及的所有分片(Set)和状态。
2. 第一个 SQL 的路由与 XA 开启 (Step 2, 3, 4)
- 动作:客户端发送
SQL1。 - 细节:
- 解析:Proxy 对 SQL 进行解析,确定数据所在的物理分片(图中为Set 1)。
- XA Start:由于是该事务第一次访问 Set 1,Proxy 先向 Set 1 发送
xa start 'gtid'指令,开启一个全局唯一的事务分支。 - 执行:接着 Proxy 将具体的
SQL1发送给 Set 1 并获得执行结果反馈。
3. 跨分片 SQL 执行 (Step 5)
- 动作:客户端发送
SQL2。 - 细节:Proxy 解析发现
SQL2需要访问Set 2。同理,它会向 Set 2 开启 XA 分支并执行。此时,Proxy 的Gtxn结构中已经记录了该事务涉及两个参与者:Set 1 和 Set 2。
4. 触发两阶段提交 (Step 6)
- 动作:客户端发送
Commit。 - 细节:Proxy 接收到提交请求,不会直接让后端提交,而是启动分布式事务 2PC 协议。
5. 第一阶段:Prepare 投票 (Step 7)
- 动作:Proxy 向所有涉及的分片(Set 1, Set 2)并行发送
Prepare commit 'gtid'指令。 - 内部逻辑:各个 Set 收到指令后,将数据修改记录到 Redo Log 且刷盘,同时锁定资源,并回复 Proxy 自己已经准备就绪(Ready)。
6. 关键节点:写入全局事务日志 (Step 8)
- 动作:Proxy 将事务状态持久化。
- 细节:当 Proxy 收到所有 Set 的 Ready 回复后,它会在主 Set(图中是 Set 1)的全局事务日志表
gtid_log_t中写入一条Commit Log。 - 核心意义:
- 决断点:一旦这条日志写入成功,在逻辑上这个事务就已经成功了。
- 容灾依据:即便此后 Proxy 宕机,新拉起的 Proxy 只要扫描
gtid_log_t发现这条记录,就会强制要求 Set 2 执行最终的 Commit,确保数据不丢失。
技术重点
- Proxy 的中枢作用:它不仅负责 SQL 路由,还管理着事务的生命周期。
Gtxn是内存中的实时状态,而gtid_log_t是磁盘上的最终真相。 - GTID (Global Transaction ID):图中的
'gtid'贯穿始终。它是分布式事务的唯一索引,确保 Proxy、Set 1、Set 2 都在讨论同一件事。 - 为什么在 Set 1 写日志?:TDSQL 通常选择事务涉及的第一个分片(或者特定的元数据节点)来记录全局日志。这样可以将分布式事务的元数据与业务数据存储在同一个数据库集群中,利用数据库自身的可靠性来保护事务日志。
- 关于原子性:图中的Step 7是原子性的保证。如果有任何一个 Set 在 Prepare 阶段失败,Proxy 就会走
XA ROLLBACK流程,而不会进入 Step 8。
这个流程图展示的是“成功提交”的前半程。在 Step 8 成功后,Proxy 还会发起XA COMMIT指令给所有 Set,完成最后的资源释放。
“Step 8 之后,即使天塌下来,这个事务也得给我提交成功”。这就是分布式事务强一致性的魅力所在。
全局事务示例
xa prepare, insert into … , xa commit三者在物理层面的操作
以下是基于 MySQL 5.7+ 开启binlog且innodb_flush_log_at_trx_commit=1、sync_binlog=1时的物理执行链路:
第一阶段:XA PREPARE <xid>
目标:将“中间状态”持久化,确保宕机可恢复且主从状态对齐。
- Engine: Undo Persistence-> 将用于回滚的 Undo 记录写入Redo Log Buffer。
- Server: Binlog Event Write-> 在内存中构造
XA_prepare_log_event,并写入OS Cache(内存 Binlog 文件)。 - Engine: Redo Log Flush (fsync)-> 调用系统
fsync,将 Redo Log(含事务状态为PREPARED及 Undo 信息)从 Buffer 强制刷入磁盘物理文件。 - Server: Binlog Flush (fsync)-> 调用系统
fsync,将 Binlog 中的XA_prepare_log_event强制刷入磁盘物理文件。 - Status: Holding Locks-> 此时在物理内存中,事务涉及的所有行锁(Row Locks)和间隙锁继续保持锁定状态,不允许释放。
第二阶段:INSERT INTO xa.gtid_log_t VALUES ('gtid-1')
目标:执行一个标准的本地事务,作为分布式一致性的“逻辑断点”。
- Buffer Pool Change-> 在 InnoDB Buffer Pool 中修改
gtid_log_t表对应的旧页,生成Dirty Page。 - Redo Log Write-> 将这一条
INSERT操作的物理改动逻辑写入Redo Log Buffer。 - Binlog Write-> 将此 SQL 对应的逻辑事件(Write_rows_log_event)写入Binlog OS Cache。
- Two-Phase Flush (Internal)-> 触发一次单机内部的 2PC:
- fsync Redo Log:确保这条“决策记录”的物理改动已落盘。
- fsync Binlog:确保这条“决策记录”的逻辑日志已落盘。
- Decision Point-> 这一步完成的瞬间,磁盘上已存在
gtid-1的持久化记录。此时分布式事务在逻辑上已不可逆转。
第三阶段:XA COMMIT <xid>
目标:修改状态标记,释放物理资源。
- Server: Binlog Commit Event-> 在 Binlog 中写入一个
XID_log_event(标记该 XID 正式提交),并调用fsync刷盘。 - Engine: Memory State Change-> 在 InnoDB 内存事务系统中,将该事务的状态位从
PREPARED修改为COMMITTED。 - Engine: Redo Log Flush (fsync)-> 在 Redo Log 中追加Commit 标记位并调用fsync。
注意:此时数据页(Dirty Page)可能还在内存里,还没刷盘,但这不影响一致性,因为 Redo Log 已落盘。
- Engine: Lock Release->最关键动作:遍历该事务所持有的所有锁对象,将其从锁管理器的链表中摘除,唤醒正在等待这些锁的其他线程。
- Engine: MVCC Update-> 更新 Read View 相关信息,使得该事务的修改对其他快照读(Snapshot Read)正式可见。
- Engine: Undo Purge-> 将该事务的 Undo Log 记录放入Purge 队列,标记为可由后台 GC 线程物理删除。
如果你还记得确保binlog和redolog一致性的XA内部两阶段提交,这里的xa prepare和xa commit与其过程如出一辙。内部两阶段提交的核心逻辑是:先写redolog并prepare,后写binlog并commit。如果commit前宕机,则根据binlog是否写入成功来决定是补上commit还是rollback。这里的xa prepare和xa commit逻辑类似,xa prepare这一步是一个完整的XA内部两阶段提交,包括刷盘redolog和binlog;INSERT INTO xa.gtid_log_t values('xa-gtid-1')这一步,其实类似于XA内部两阶段提交中的,写入binlog的步骤。它的核心含义是,写入一个全局的信息,并以其内容为准,如果后续宕机,通过判断该表中是否有记录,反映本机事务是否执行成功,来决定是commit还是rollback
这样的操作顺序能防止出现什么问题?比如某关键阶段执行失败,会发生什么
场景一:故障发生在“决策点”之前
故障点:Set 1 执行完了XA PREPARE,但在向gtid_log_t插入记录之前,Proxy 挂了。
- 物理现状:
- Set 1 的磁盘里有
Redo(Prep)和Binlog(Prep)。 gtid_log_t表里没有记录。
- Set 1 的磁盘里有
- 恢复逻辑:
- 新 Proxy 接管,扫描所有分片,发现 Set 1 处于
PREPARED状态。 - 新 Proxy 去查
gtid_log_t记录,结果:查无此项。 - 判决:因为没有全局决策证据,Proxy 命令 Set 1 和 Set 2 执行
XA ROLLBACK。
- 新 Proxy 接管,扫描所有分片,发现 Set 1 处于
- 防止了什么问题?
防止了虚假承诺。即使本地已经把日志写好了,但只要没拿到“全局准考证”,就绝对不能提交。这样保证了在 Proxy 意外崩溃时,宁可全部回滚,也不让数据处于不确定的中间态。
场景二:故障发生在“决策点”之后
故障点:插入gtid_log_t成功了,但在执行动作XA COMMIT之前,Set 2 突然宕机了。
- 物理现状:
gtid_log_t表里已有记录。- Set 1 已准备好,Set 2 还没收到正式提交指令就炸了。
- 恢复逻辑:
- Set 2 重启,启动崩溃恢复流程。
- Set 2 发现本地有
PREPARED事务,但没看到自己的COMMIT标记。 - Set 2 并不急着回滚,而是等待指令。
- Proxy 扫描到
gtid_log_t有记录,立刻向 Set 2 补发XA COMMIT。
- 防止了什么问题?
防止了半途而废。只要动作 3 写入成功,就相当于在全集群发了一份**“必达令”**。哪怕 Set 2 断电一年,一年后重启,它也必须根据这张“存根”把没干完的活干完(提交),确保 A 扣了钱 B 必须能加上。
场景三:针对“内部 XA”顺序
故障点:在XA PREPARE阶段,动作 1(Redo Prep)写成功了,但在动作 2(Binlog Prep)写入时,磁盘瞬间写满了。
- 物理现状:
- Redo Log 里记录了修改和
XA PREPARED。 - Binlog 里没有任何该事务的记录。
- Redo Log 里记录了修改和
- 恢复逻辑:
- MySQL 重启,进行内部对账。
- 发现 Redo Log 里有个“待定”事务。
- 去翻 Binlog 发现“一片空白”。
- 判决:因为 Binlog 没写进去,说明从库(Slave)绝对没收到这笔数据。如果主库提交了,主从就彻底不一致了。
- 动作:主库利用 Redo Log 里的 Undo 信息,强制回滚该本地事务。
- 防止了什么问题?
防止了主从脱节。这保证了主库的“自愈”始终以“全集群能看到(Binlog)”为准。