gorm事务内调rpc必然破坏原子性,因本地事务仅管控数据库连接,而rpc跨进程网络不可控;tcc是解法之一,但try阶段须与gorm事务绑定完成db层资源冻结,confirm/cancel必须幂等、可重试、无副作用。

直接说结论:GORM 事务内不能直接调用 RPC,否则必然破坏原子性;TCC 是解法之一,但必须把 Try 阶段的资源预留逻辑下沉到数据库层,且 Confirm/Cancel 必须可重试、幂等、无副作用。
为什么 GORM 事务里调 RPC 必然不一致
GORM 的 Transaction 只管本地 MySQL/TiDB 连接,RPC 调用是跨进程、跨网络的独立操作。一旦你在 db.Transaction() 里执行 paymentClient.Deduct(ctx, req),就出现两个事实上的“事务边界”:一个在本地 DB,一个在远程服务。网络超时、服务宕机、重试乱序都会导致状态撕裂——比如库存已扣、支付 RPC 成功但返回丢包,GORM 事务回滚了,而支付却已生效。
常见错误现象:
- 日志里看到
context deadline exceeded后,DB 回滚成功,但下游服务实际已完成扣款 - 手动重试订单创建,因幂等键缺失,导致重复扣款 + 重复扣库存
- 对账脚本每天报“支付成功但订单未确认”,根源就是 RPC 调用游离在事务外
TCC 的 Try 阶段必须和 GORM 事务绑定
TCC 不是加个注解就完事,关键在 Try 阶段——它得在本地事务中完成资源冻结,并确保该冻结能被 Confirm/Cancel 精确识别和操作。以扣余额为例:
- Try 阶段不是调 RPC,而是执行
UPDATE accounts SET frozen_balance = frozen_balance + ? WHERE user_id = ? AND balance >= ?,且该 SQL 必须和订单插入、库存预占写在同一db.Transaction()里 - 必须用 CAS(如
AND balance >= ?)保证余额充足校验与冻结原子执行,避免竞态 - 冻结记录要带唯一
tx_id(即全局事务 ID),后续 Confirm/Cancel 全靠它定位,不能只靠业务单号 - Try 成功后,立即写一条本地
tcc_transaction_log表,状态为TRYING,含tx_id、服务名、参与方地址、时间戳
Confirm/Cancel 怎么做到真正可靠
Confirm 和 Cancel 是异步被协调器(如 DTM 或自研)回调的,它们本身不能再依赖原 GORM 事务上下文,但必须满足三个硬约束:
- 幂等性:用
INSERT IGNORE INTO tcc_compensate_log (tx_id, phase) VALUES (?, 'CONFIRM')做首次标记,再查再执行直接跳过 - 空回滚防护:Cancel 被先调(Try 没来得及写 log 就失败),需检查
tcc_transaction_log是否存在对应tx_id,不存在则拒绝执行 - 悬挂处理:Try 成功写了 log,但 Confirm 超时未收到,后续 Cancel 又被误触发——此时需在 Cancel 里加状态判断:仅当 log.state == TRYING 时才释放,否则跳过
- 所有 SQL 操作必须显式指定
context.WithTimeout(ctx, 5*time.Second),防止卡死阻塞协调器
DTM + Hyperf/gRPC 场景下容易漏掉的细节
如果你用 dtm-client 配合 Hyperf,注意这些非文档但必踩的坑:
-
dtm-server默认通过 HTTP 回调服务,但 Hyperf 的gRPC服务不暴露 HTTP 接口,必须额外起一个api服务专门接收 DTM 回调,或改用 gRPC 回调模式(需自定义dtm-client的回调 client) - Hyperf 中
DtmMiddleware会自动注入xid,但rpc调用默认不透传 header,需在 client 侧手动加metadata.AppendToOutgoing(ctx, "xid", xid) - GORM 初始化时若没设
SetConnMaxLifetime(30 * time.Second),TiDB 连接空闲超时后,Confirm 阶段可能遇到connection refused,导致补偿失败 - 不要在 Try 阶段做任何耗时操作(如发消息、调第三方 API),它必须是纯 DB 冻结,否则整个 TCC 流程延迟飙升
最常被忽略的一点:TCC 的“一致性”不来自技术协议,而来自业务状态机设计。比如冻结余额后,用户查询余额应显示“可用余额 - 冻结额”,这个计算逻辑必须在所有读接口统一实现,否则用户看到的数据永远是割裂的。











