直接用 database/sql 做 tcc 会失败,因其仅支持单库 acid,无法跨服务协调、不保证 try/confirm/cancel 各阶段的幂等性与独立提交,且缺乏全局事务视图和补偿机制。

为什么直接用 database/sql 做 TCC 会失败
TCC(Try-Confirm-Cancel)本质是业务层面的分布式事务协议,不是数据库事务的延伸。Go 标准库的 sql.Tx 只能保证单库 ACID,无法跨服务协调状态,更不能自动回滚已提交的 Try 阶段操作(比如已扣减库存、已冻结金额)。强行把 Confirm/Canel 写成 SQL 回滚语句,会导致状态不一致——例如 Try 中调用下游 HTTP 扣减成功,但本地 DB 事务失败,此时没有“全局视图”来触发 Cancel。
关键点在于:TCC 的每个阶段(Try/Confirm/Cancel)都必须是**幂等、可重试、独立提交**的业务操作,不能依赖数据库事务自动回滚。
- Try 阶段要预留资源(如冻结账户余额),并持久化「事务上下文」(含全局事务 ID、分支 ID、参数快照)
- Confirm/Canel 必须根据上下文重放,而非靠数据库回滚
- 没有中心事务协调器(如 Seata Server)时,需自行实现事务日志表 + 定时扫表补偿
github.com/yedf/dtm 是目前最省心的 Go TCC 方案
自己从零写 TCC 协调器容易漏掉幂等校验、悬挂事务、空回滚等边界问题。dtm 提供了成熟的 Go SDK 和独立的协调服务,对业务代码侵入小,且支持本地消息表、SAGA、TCC 多种模式。
实操要点:
- 在 Try 接口里调用
dtmcli.TccGlobalTransaction开启全局事务,传入 Confirm/Cancel 的 HTTP 地址 - Confirm/Cancel 接口必须接收
gid(全局事务 ID)和trans_type,并基于gid做幂等判断(建议用唯一索引约束) - Try 阶段返回失败时,
dtm会自动跳过 Confirm,只调 Cancel;Confirm 失败则持续重试(默认 30 分钟),直到成功或人工介入 - 不要在 Confirm/Cancel 里做「查询再更新」,必须用
UPDATE ... WHERE gid = ? AND status = 'trying'类型的原子更新
示例 Confirm 实现片段:
func Confirm(c *gin.Context) {
gid := c.Query("gid")
// 幂等:只更新未 confirm 的记录
_, err := db.Exec("UPDATE accounts SET frozen = 0, balance = balance - ? WHERE gid = ? AND status = 'trying'", amount, gid)
if err != nil {
c.JSON(500, "confirm failed")
return
}
c.JSON(200, "success")
}
自己实现轻量 TCC 时,transaction_log 表结构不能少
没有 dtm 这类中间件时,必须用数据库持久化事务状态,否则节点宕机后无法恢复。这张表不是可选的,是 TCC 可靠性的底线。
最小必要字段:
-
gid:全局事务 ID(如 UUID),主键 -
branch_id:分支 ID(服务实例+时间戳生成) -
status:枚举值try_succeed/confirm_succeed/cancel_succeed/confirm_failed -
try_req:JSON,存 Try 请求参数(用于 Cancel 重放) -
create_time、update_time
定时任务每 30 秒扫描 status = 'try_succeed' AND update_time 的记录,调用 Confirm;若 Confirm 连续失败 5 次,改状态为 <code>confirm_failed 并告警——这个超时窗口必须比业务最长处理时间长,否则可能误判。
Cancel 阶段最容易忽略的三个细节
Cancel 看似只是 Try 的反向操作,但实际出错率远高于 Confirm。常见翻车点:
- Try 成功但网络原因没收到响应,业务层认为 Try 失败而没记日志,后续 Cancel 无据可查 → 解决办法:Try 必须先写日志再发远程调用,用本地消息表+可靠发送兜底
- Cancel 时原 Try 参数已失效(如商品下架),导致释放资源失败 → 解决办法:Try 日志中保存足够上下文(如 sku_id、冻结时间、有效期),Cancel 前先校验资源有效性
- 并发 Cancel:同一
gid被多次触发 → 解决办法:Cancel 接口第一行必须是UPDATE transaction_log SET status = 'cancel_succeed' WHERE gid = ? AND status = 'try_succeed',检查影响行数是否为 1
没有事务协调器时,Cancel 的健壮性几乎决定了整个 TCC 方案能否在线上存活。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











