tcc在go中本质是业务契约,框架仅负责调度和重试,try/confirm/cancel的正确性完全依赖开发者在sql中实现状态校验与cas更新、本地事务控制、超时处理及主键优化等细节。

Go里TCC不是框架能自动完成的事
你写dtmcli.TccGlobalTransaction或seata-go的注册代码,不代表事务就可靠了。TCC在Go中本质是业务契约——框架只管调度和重试,Try是否真冻结了资金、Confirm是否真的只扣冻结额、Cancel是否解冻且不误删记录,全靠你自己在SQL里写对条件。
常见错误现象:panic: call to Confirm after Try failed,根本原因是没在Confirm前查tx_log.status = 'trying';或者Confirm被重复调用导致双扣,因为数据库没加WHERE id = ? AND status = 'trying'的CAS更新。
- 所有
Try必须是本地事务,且只更新预留字段(如balance_freeze),不能动balance -
Confirm和Cancel必须带context.Context并设超时,避免卡死整个事务链 - 不要用
uuid当tx_log主键,高并发下INSERT会成瓶颈;推荐用shard_id + timestamp组合或Snowflake ID
dtm或seata-go只是调度器,不是业务逻辑代理
你引入github.com/dtm-labs/dtmcli,只是把Try请求发给业务服务,它不会帮你检查账户余额、不会替你写UPDATE accounts SET frozen = frozen + ? WHERE uid = ?。真正决定“冻结多少”“扣减是否合法”的,是你自己写的HTTP handler里的判断逻辑。
使用场景:跨服务转账时,dtm Server只负责按顺序调用A服务的/try、B服务的/try,都成功后再并发调/confirm。但A服务的/confirm接口里,必须自己查tx_log确认状态,再执行UPDATE accounts SET balance = balance - ?, frozen = frozen - ?。
- dtm要求你的
Try接口返回HTTP 200才认为成功;返回4xx/5xx或超时,dtm会触发所有已Try服务的Cancel - seata-go的
TmClient需要你在每个微服务里手动注入BranchSession,透传xid,不能依赖context.WithValue - 别指望框架自动兜底:服务重启后,dtm不会主动扫描你本地的
tx_log表;得自己写定时任务查status = 'trying' AND created_at
日志表设计直接影响TCC能否落地
tx_log不是可有可无的辅助表,它是TCC状态机的唯一真相源。没有它,Confirm和Cancel就变成盲操作;设计不好,整条事务链路IO直接翻倍。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
性能影响:一次Try要写两次DB——业务表+日志表;Confirm若没加WHERE status = 'trying',可能把已Confirm的记录再扣一遍;Cancel若没校验status IN ('trying', 'confirmed'),可能对已失败的事务重复解冻。
- 表结构至少包含:
id(业务唯一键,非uuid)、gid(全局事务ID)、branch_id、status(枚举值:trying/confirmed/cancelled)、created_at、updated_at - 关键索引:
INDEX idx_gid_status (gid, status)用于Confirm/Cancel快速定位,INDEX idx_timeout (status, created_at)用于异步恢复扫描 - 别把日志存在内存或Redis里——宕机即丢状态;SQLite在单机场景够用,但微服务部署必须用MySQL/PostgreSQL
幂等和超时控制必须在SQL层实现
Go的sync.Map或redis.SetNX只能防瞬时重复,扛不住服务重启、网络重传、dtm重试。真正的幂等必须落在数据库UPDATE的WHERE条件里。
容易踩的坑:写UPDATE tx_log SET status = 'confirmed' WHERE gid = ?,结果Confirm被调10次,日志状态变10次,但业务数据只扣一次——表面看没问题,实际掩盖了重试失控风险。
- Confirm正确写法:
UPDATE tx_log SET status = 'confirmed', updated_at = NOW() WHERE gid = ? AND status = 'trying' - Cancel正确写法:
UPDATE tx_log SET status = 'cancelled', updated_at = NOW() WHERE gid = ? AND status IN ('trying', 'confirmed') - 所有SQL必须配
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second),并在db.ExecContext(ctx, ...)里传入 - 网络调用(如调其他服务Confirm)失败时,不能直接return error;要先记
status = 'confirm_failed',再交由后台任务重试
真实项目里最常被忽略的,是tx_log表的清理策略和Confirm的锁等待时间。没人告诉你,当库存服务Confirm卡在SELECT FOR UPDATE上30秒,dtm早已超时回滚,而你的库存表还挂着行锁——这种隐性死锁不会报错,但会拖垮后续所有Try请求。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










