echo框架不提供tcc事务逻辑,所有try/confirm/cancel需手写:try须先落日志再业务操作且原子执行;confirm/cancel须查状态、校验、更新三步缺一不可;需定时扫描兜底未完成事务;tx_id必须全局透传。

Echo 框架本身不提供、也不能替代 TCC 分布式事务逻辑——它只是 HTTP 路由层,所有 Try/Confirm/Cancel 的语义、状态校验、幂等控制、日志持久化,都得你手写业务代码实现。
Try 接口必须先落日志再操作业务,且用本地事务包裹
很多人在 Echo 的 handler 里直接调用扣款或冻结逻辑,然后才写事务日志,结果服务崩溃后日志丢失,后续无法判断该事务是否已 Try。这会导致 Confirm 空执行或 Cancel 漏触发。
正确做法是:在一个 sql.Tx 中,先 INSERT 一条 tcc_transactions 记录(status = 'trying'),再执行业务预留(如 UPDATE accounts SET frozen_balance = frozen_balance + ?)。两步必须原子完成。
- 别用
db.Exec单独写日志,否则和业务更新不在同一事务中 - INSERT 日志时主键别用
uuid.NewString(),高并发下索引分裂严重;推荐snowflake.ID()或time.Now().UnixNano()+ 服务实例 ID 拼接 - 日志表必须有复合索引:
INDEX idx_status_created (status, created_at),支撑定时扫描
Confirm 和 Cancel 必须查日志状态再执行,不能无条件调用
常见 panic:call to Confirm after Try failed,根源就是 Confirm handler 里没查 tcc_transactions 表就直接去扣余额。
每个 Confirm/Cancel handler 开头都得做三件事:SELECT FOR UPDATE 查日志、校验 status 是否为 'trying'、再更新业务表和日志状态。漏掉任何一步都可能资损。
- SQL 更新必须带 WHERE 条件:
UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND frozen_balance >= ?,防止重复扣减 - Confirm/Cancel 的 DB 操作必须用
ExecContext(ctx, ...),并传入context.WithTimeout(ctx, 3*time.Second),避免锁等待卡死整个 Echo 请求链路 - 别在 handler 里直接发 HTTP 调用下游服务——先落本地 saga_records 表,再由后台 worker 异步驱动
Echo 启动时必须注册定时任务兜底未完成事务
HTTP 请求失败、服务重启、goroutine 被 cancel,都会导致 Confirm/Cancel 没被调用。TCC 没有协调器,只能靠自己扫表驱动。
在 main.go 启动 Echo 之后,起一个独立 goroutine,定期执行:
SELECT * FROM tcc_transactions WHERE status = 'trying' AND created_at <p>对每条记录,根据当前时间与 <code>created_at</code> 差值决定补调 Confirm 还是 Cancel(比如 30s 内补 Confirm,超 60s 补 Cancel)。</p>
- 扫描 SQL 必须加
FOR UPDATE SKIP LOCKED(MySQL 8.0+)或手动加分布式锁,避免多实例重复处理同一条记录 - 别依赖
time.Now()做超时判断——节点间时钟漂移会导致 Cancel 被跳过;统一用数据库的NOW()值做比较 - 这个 goroutine 的错误日志必须单独打,不能混进 Echo access log,否则故障排查时找不到线索
最复杂也最容易被忽略的点是:TCC 的“事务 ID”不是请求 ID,也不是 trace ID,而是跨服务共享的、全局唯一的 tx_id 字符串。它要透传到所有下游服务的 Try/Confirm/Cancel 接口,并作为日志表和业务表的关联键。少一次透传,整个补偿链就断了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











