bigywater.tech
分布式事务 · 22

事务协调者、管理者与资源管理者:角色分工(手写原型)

TM、RM、TC 三种角色分工是什么?各存什么状态?TM 崩溃、TC 崩溃、RM 二阶段失败时谁来收尾?这篇用我手写的三个独立进程(HTTP + MySQL)把这些分工和故障路径逐个跑出来。它不是 Seata,但结构一致。

12 次
一个两分支的全局事务在三种角色之间产生的 HTTP 调用(TM 自己发起 4 次)
1577 ms
TM 崩溃后,TC 超时扫描器在设置 1500 ms 超时后把全局事务回滚的触发时刻
1034 ms
RM 二阶段一直失败时,另一个全局事务等全局锁最终失败的时间

各角色分工

实测 · 本机执行输出
         14 ms  [TC] TM → TC  begin xid-1792248455324005000(超时 60000 ms)
         28 ms  [TC] RM(acct_db) → TC  register 分支 1 到 xid-1792248455324005000
         43 ms  [TC] RM → TC  lock acct_db.account:1 给 xid-1792248455324005000(等了 7 ms)
         50 ms  [TC] RM → TC  report 分支 1 = PHASE1_DONE
         51 ms  [RM:acct_db] TM → RM(acct_db)  业务调用:account#1 {'id': 1, 'balance': 1000} → {'id': 1, 'balance': 900},本地已提交,分支 1 报告 PHASE1_DONE
         65 ms  [TC] RM(stock_db) → TC  register 分支 2 到 xid-1792248455324005000
         80 ms  [TC] RM → TC  lock stock_db.stock:1 给 xid-1792248455324005000(等了 7 ms)
         87 ms  [TC] RM → TC  report 分支 2 = PHASE1_DONE
         88 ms  [RM:stock_db] TM → RM(stock_db)  业务调用:stock#1 {'id': 1, 'qty': 50} → {'id': 1, 'qty': 47},本地已提交,分支 2 报告 PHASE1_DONE
         95 ms  [TC] TM → TC  commit xid-1792248455324005000(状态 → COMMITTING,二阶段由 TC 的后台线程去做)
        124 ms  [RM:acct_db] RM(acct_db)  branch_commit:删除 undo_log(xid-1792248455324005000)
        124 ms  [TC] TC → RM(acct_db)  branch_commit 分支 1 成功(第 1 次)
        132 ms  [RM:stock_db] RM(stock_db)  branch_commit:删除 undo_log(xid-1792248455324005000)
        133 ms  [TC] TC → RM(stock_db)  branch_commit 分支 2 成功(第 1 次)
        140 ms  [TC] TC  全局事务 xid-1792248455324005000 → COMMITTED(全局锁已释放)
  TM 的 commit 调用返回后 78 ms 全局事务才真正 COMMITTED(二阶段是 TC 在后台做的);账户 900、库存 47;undo_log 行数 0 + 0;tc_db:global_tx 1 行、branch 2 行、global_lock 0 行

TM 决定开始、提交或回滚;RM 做本地分支、注册、报告,并响应二阶段回调;TC 保存全局事务与分支状态、全局锁,驱动二阶段。

TM 崩溃

实测 · 本机执行输出
  两个分支做完后:账户 900、库存 47,全局事务状态 BEGIN
          8 ms  [TC] TM → TC  begin xid-1792248455601517000(超时 1500 ms)
         16 ms  [TC] RM(acct_db) → TC  register 分支 3 到 xid-1792248455601517000
         39 ms  [TC] RM → TC  report 分支 3 = PHASE1_DONE
         39 ms  [RM:acct_db] TM → RM(acct_db)  业务调用:account#1 {'id': 1, 'balance': 1000} → {'id': 1, 'balance': 900},本地已提交,分支 3 报告 PHASE1_DONE
         47 ms  [TC] RM(stock_db) → TC  register 分支 4 到 xid-1792248455601517000
         69 ms  [TC] RM → TC  report 分支 4 = PHASE1_DONE
         70 ms  [RM:stock_db] TM → RM(stock_db)  业务调用:stock#1 {'id': 1, 'qty': 50} → {'id': 1, 'qty': 47},本地已提交,分支 4 报告 PHASE1_DONE
       1577 ms  [TC] TC  超时扫描:xid-1792248455601517000 一直没有收到 TM 的决定 → ROLLING_BACK
       1595 ms  [RM:acct_db] RM(acct_db)  branch_rollback:按 undo_log 恢复 [('account', '1')],删除 undo_log
       1595 ms  [TC] TC → RM(acct_db)  branch_rollback 分支 3 成功(第 1 次)
       1606 ms  [RM:stock_db] RM(stock_db)  branch_rollback:按 undo_log 恢复 [('stock', '1')],删除 undo_log
       1607 ms  [TC] TC → RM(stock_db)  branch_rollback 分支 4 成功(第 1 次)
       1607 ms  [TC] TC  全局事务 xid-1792248455601517000 → ROLLED_BACK(全局锁已释放)
  开始后 1628 ms 左右 TC 把它回滚完(等待了 1536 ms 才变成 ROLLED_BACK);账户 1000、库存 50,global_lock 行数 0

没人决定的全局事务,由 TC 超时扫描器回滚,RM 按 undo_log 恢复。

TC 崩溃

实测 · 本机执行输出
  [commit] TC 被 kill -9 时全局事务状态是 COMMITTING(二阶段没做完);此刻 undo_log 行数 1 + 1,global_lock 行数 2,账户 900、库存 47
        140 ms  [TC] TC  启动恢复:库里有 1 个没做完二阶段的全局事务 [('xid-1792248457309379000', 'COMMITTING')],继续做
        420 ms  [TC] TC → RM(acct_db)  branch_commit 分支 5 成功(第 4 次)
        429 ms  [TC] TC → RM(stock_db)  branch_commit 分支 6 成功(第 4 次)
        437 ms  [TC] TC  全局事务 xid-1792248457309379000 → COMMITTED(全局锁已释放)
  [commit] TC 重启后 219 ms 把二阶段做完:状态 COMMITTED,账户 900、库存 47,undo_log 行数 0 + 0,global_lock 行数 0
  [rollback] TC 被 kill -9 时全局事务状态是 ROLLING_BACK(二阶段没做完);此刻 undo_log 行数 1 + 1,global_lock 行数 2,账户 900、库存 47
        135 ms  [TC] TC  启动恢复:库里有 1 个没做完二阶段的全局事务 [('xid-1792248458275693000', 'ROLLING_BACK')],继续做
        162 ms  [TC] TC → RM(acct_db)  branch_rollback 分支 7 成功(第 3 次)
        170 ms  [TC] TC → RM(stock_db)  branch_rollback 分支 8 成功(第 3 次)
        177 ms  [TC] TC  全局事务 xid-1792248458275693000 → ROLLED_BACK(全局锁已释放)
  [rollback] TC 重启后 10 ms 把二阶段做完:状态 ROLLED_BACK,账户 1000、库存 50,undo_log 行数 0 + 0,global_lock 行数 0

TC 的状态在库里,重启后扫描 COMMITTING / ROLLING_BACK 继续二阶段,分支的重试次数也保留。

RM 二阶段失败

实测 · 本机执行输出
         80 ms  [TC] TC → RM(acct_db)  branch_rollback 分支 9 失败(第 1 次):HTTP Error 500: Internal Server Error;100 ms 后重试
         87 ms  [RM:stock_db] RM(stock_db)  branch_rollback:按 undo_log 恢复 [('stock', '1')],删除 undo_log
         88 ms  [TC] TC → RM(stock_db)  branch_rollback 分支 10 成功(第 1 次)
        186 ms  [TC] TC → RM(acct_db)  branch_rollback 分支 9 失败(第 2 次):HTTP Error 500: Internal Server Error;200 ms 后重试
        395 ms  [TC] TC → RM(acct_db)  branch_rollback 分支 9 失败(第 3 次):HTTP Error 500: Internal Server Error;400 ms 后重试
        815 ms  [RM:acct_db] RM(acct_db)  branch_rollback:按 undo_log 恢复 [('account', '1')],删除 undo_log
        815 ms  [TC] TC → RM(acct_db)  branch_rollback 分支 9 成功(第 4 次)
        816 ms  [TC] TC  全局事务 xid-1792248459039689000 → ROLLED_BACK(全局锁已释放)
  账户 1000、库存 50;最终状态 ROLLED_BACK
  账户 RM 一直失败 4 秒:全局事务状态 ROLLING_BACK,TC 重试了 6 次(间隔 100 → 200 → 400 → 800 → 1600 ms 封顶),账户 900(分支已经提交的 -100 还没恢复),global_lock 仍有 2 行
  另一个全局事务想改同一个账户:等了 1034 ms 后失败(LockConflict: acct_db.account:1 的全局锁被 xid-179224845995621800)—— 没做完二阶段的事务一直占着全局锁
  把故障去掉后,TC 的下一次重试成功:最终状态 ROLLED_BACK,账户 1000

TC 带退避重试;一直失败则事务停在二阶段,全局锁一直占着,别的事务等锁失败。

成本

实测 · 本机执行输出
  TM 线程数 | 全局事务数 | TM 看到的单个事务耗时(中位数,begin 到 commit 返回) | 每秒完成的全局事务(到 commit 返回) | 全部二阶段做完的总用时
  -------+-------+-------------------------------------+------------------------+------------
  1      | 100   | 71.4 ms                             | 14                     | 7.34 s     
  4      | 200   | 90.9 ms                             | 42                     | 4.78 s     
  8      | 200   | 147.0 ms                            | 53                     | 3.84 s     
  单线程跑 50 个全局事务(每个 2 个分支)时,各进程收到的 HTTP 调用次数(平均每个全局事务):TC/begin 1.0,  TC/commit 1.0,  TC/lock 2.0,  TC/register 2.0,  TC/report 2.0,  库存 RM/branch_commit 1.0,  库存 RM/do 1.0,  账户 RM/branch_commit 1.0,  账户 RM/do 1.0
  合计每个全局事务 12.0 次 HTTP 调用(本地事务是 0 次);其中 TM 直接发起的是 begin、两次业务调用、commit 共 4 次,其余是 RM 与 TC 之间互相调用
  做完之后各角色库里留下的行数:TC 的 global_tx 50 行(状态 COMMITTED 的历史记录)、branch 100 行,global_lock 0 行(锁已释放);RM 的 undo_log 0 + 0 行(二阶段清理掉了);TM 不存任何状态

每个全局事务多 12 次跨进程调用,TC 里留下历史行;耗时数字只代表这个原型。

分步演示

TM RM TC
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.7.0 与 PostgreSQL 16.15(Homebrew,每个实验自己 initdb、自己起实例,端口 9150 以上,只监听 127.0.0.1)/ Python 3.14.8 + PyMySQL 1.2.3 + psycopg 3.3.6 / Redis 8.10.2 / Kafka 4.3.1 / RabbitMQ 4.3.6 / etcd 3.7.2(按各篇注明用到哪些);不连任何外部服务,不碰本机已有的数据库实例;故障用 kill -9 进程、让连接断开或在代码里的指定位置抛异常来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇是我自己写的原型(tools/articles/tcsvc.py + at.py,Python 的 http.server + urllib + PyMySQL),TC / 账户 RM / 库存 RM 是三个独立进程,TC 的状态在 MySQL 26.7.0 的 tc_db 里,TM 是实验脚本;二阶段由 TC 的线程池驱动(8 个线程),重试退避 100 ms 起翻倍上限 1.6 s;事件日志是各进程各自追加写的文件,时间戳是写入时刻;“被 kill -9”的是 TC 进程
    • 没有验证:Seata TC 的实际实现(Raft 集群、数据库存储模式、全局会话的存储格式);TC 多副本时的脑裂与选主;TM 崩溃后的幂等重试;RM 注册分支的时机(这里是业务调用的开头注册);事件日志里个别毫秒数的时钟精度(同一台机器);大量并发下的 TC 瓶颈(原型的吞吐数字只代表这个 Python 实现)
    京ICP备2026067502号-1京公网安备11010502063487号