mysql xa在分库场景基本不可用,因协调者崩溃后无法全局判断分支状态,且各节点版本、binlog、时钟等不一致导致xa commit失败;seata at需手动建全字段undo_log表;最终一致性方案更可控。

MySQL XA 事务为什么在分库场景下基本不可用
XA 协议理论上支持跨库两阶段提交,但实际在分库(比如 ShardingSphere 或自研分片)中几乎无法稳定落地。根本原因不是协议本身错,而是协调者(TM)和参与者(RM)的故障恢复能力严重不对等。
- MySQL 的
XA RECOVER只能查出本实例上未完成的 XA 分支,一旦 coordinator 崩溃,没有全局视图,无法判断哪些分支该 commit、哪些该 rollback - 分库环境下,不同物理库的 MySQL 版本、binlog 格式、server_id 甚至时钟可能不一致,导致
XA PREPARE成功后,某节点在崩溃恢复时拒绝XA COMMIT - ShardingSphere 等中间件默认关闭 XA 支持,开启后需额外部署
Seata AT或Atomikos,但它们对 MySQL 的 XA 日志解析有版本限制(例如不兼容 MySQL 8.0.33+ 的 redo log 优化)
Seata AT 模式下 undo_log 表必须手动建且字段不能少
Seata 的 AT 模式靠本地 undo_log 表记录回滚日志,但它不会自动建表,也不校验表结构。很多团队上线后才发现事务卡住或回滚失败,根源是字段缺失或类型不匹配。
- 必须包含
branch_id(BIGINT NOT NULL)、xid(VARCHAR(128) NOT NULL)、context(VARCHAR(128) NOT NULL)、rollback_info(LONGBLOB NOT NULL)、log_status(TINYINT NOT NULL)、log_created(DATETIME NOT NULL)、log_modified(DATETIME NOT NULL) -
rollback_info类型必须是LONGBLOB,用TEXT会导致大事务序列化失败,错误信息类似java.io.EOFException: Unexpected end of ZLIB input stream - 每个分库都要建同名同结构的
undo_log表,Seata 不支持跨库路由该表
本地消息表 + 最终一致性比强一致更可控
真正线上扛量的分布式事务,90% 以上走的是“可靠消息 + 对账补偿”,而不是试图用框架模拟 ACID。它不依赖数据库层的复杂协调,出问题时可查、可重试、可人工介入。
- 消息表必须和业务表在同一库同一事务中写入,用
INSERT INTO msg_table (...) VALUES (...)+UPDATE biz_table SET status = 'sent' WHERE ...包在同一个BEGIN/COMMIT里 - 投递服务轮询时要用
select ... for UPDATE SKIP LOCKED,避免多个 worker 重复取同一条消息;更新状态时要带条件,如UPDATE msg_table SET status = 'sending' WHERE id = ? AND status = 'ready' - 不要把重试逻辑全交给 MQ(比如 rabbitmq 的死信队列),它的重试间隔不可控,且无法关联原始业务上下文;应由独立的调度服务管理重试次数、退避策略和告警阈值
TCC 接口的 Confirm/Cancel 必须幂等且不依赖 try 阶段状态
TCC 是最灵活也最容易翻车的模式。很多人以为只要三个接口写出来就完事了,结果压测时大量 Confirm 失败,日志里反复出现 no try record found for xid。
-
Confirm和Cancel必须设计成纯幂等操作:比如扣库存用UPDATE stock SET qty = qty - ? WHERE sku_id = ? AND qty >= ?,而不是先SELECT再判断 - 不能假设
Try一定成功或已落库——网络超时、应用重启都可能导致Try执行了但没返回响应,此时 Seata 会直接发Confirm,你的接口必须能凭xid和业务参数自行判断是否该执行 -
Cancel不是Try的逆操作,而是“确保最终不发生”。例如Try预占额度,Cancel就是释放预占,哪怕预占记录已丢失,也要确保用户额度不被多扣
分布式事务真正的难点从来不在怎么写代码,而在于怎么定义“事务边界”——哪些操作必须原子,哪些可以妥协,以及当系统部分失联时,你愿意接受多久的数据不一致。这些决策没法靠框架自动做。