tcc在go微服务中需手动实现try/confirm/cancel逻辑并严格校验状态与幂等性:try须先落日志再操作业务,confirm/cancel必须查日志状态后执行,依赖定时任务兜底,主键宜用snowflake id,日志表应独立部署。

TCC 在 Go 微服务里不是“配个库就能跑”,而是必须自己写 Try/Confirm/Cancel 逻辑,且每个阶段都要带状态校验和幂等控制——否则 Confirm 重复执行、Cancel 漏触发、Try 失败后仍被 Confirm,都是线上资损的直接原因。
Try 阶段必须先落日志再操作业务
常见错误是先改业务表(比如 UPDATE account SET balance = balance - 100),再写事务日志。一旦服务宕机或网络中断,日志没写成,后续无法判断该事务是否已 Try,Confirm/Cancle 就失去依据。
- 正确顺序:用本地事务原子写入
tcc_transactions表(status = 'trying'),再执行冻结逻辑(如UPDATE account SET frozen_balance = frozen_balance + 100 WHERE user_id = ?) - 必须用
FOR UPDATE或 CAS 更新日志表,避免并发写入覆盖状态,例如:UPDATE tcc_transactions SET status = 'trying' WHERE id = ? AND status = 'init' - Try 接口返回前,确保日志已刷盘(尤其用 SQLite 或 BadgerDB 时,需显式调用
Sync())
Confirm 和 Cancel 必须查日志状态再执行
不查日志直接操作业务表,会导致“空回滚”(Try 没执行却 Cancel)或“悬挂”(Try 成功但 Confirm 被跳过,资源一直冻结)。
- Confirm 前必须
SELECT ... FOR UPDATE查tcc_transactions,确认status = 'trying';成功后更新业务表并把日志设为'confirmed' - Cancel 同理,只对
status = 'trying'的记录生效,释放冻结后设为'cancelled' - 两个接口都得支持重试:SQL 条件里带上状态约束,比如
UPDATE account SET frozen_balance = frozen_balance - 100 WHERE user_id = ? AND frozen_balance >= 100
没有自动协调器,就得靠定时任务兜底
Go 服务重启后,没人主动触发 Confirm/Cancel,靠 RPC 调用链传递状态根本不可靠——context 会丢、goroutine 会断、HTTP/gRPC 请求可能超时失败。
- 必须起独立 goroutine,定期扫描
tcc_transactions中status = 'trying'且created_at 的记录 - 对每条记录,根据当前时间与创建时间差决定补调 Confirm 还是 Cancel(例如超时 60s 未 Confirm 就 Cancel)
- 扫描 SQL 要加
LIMIT和SKIP LOCKED(MySQL 8.0+)或SELECT FOR UPDATE NOWAIT,避免多个实例同时处理同一条记录
别用 UUID 当事务主键,高并发下 INSERT 会成瓶颈
很多 demo 直接用 uuid.NewString() 生成 tx_id,结果在压测时 tcc_transactions 表 INSERT QPS 上不去,拖慢所有 Try 请求。
- 推荐组合主键:
tx_id用 snowflake ID(毫秒级时间戳 + 机器ID + 序列号),保证单调递增、无锁生成 - 索引要覆盖常用查询:比如
(tx_id, status, created_at),避免 Confirm/Cancel 时全表扫 - 日志表别放业务库:单独建
tcc_log库,用轻量存储(如 BadgerDB)或专库专用,防止事务日志 IO 影响核心业务
最麻烦的从来不是写三个函数,而是让每个 Confirm 都能回答“我这次执行有没有效”——这依赖日志状态、SQL 条件、数据库事务隔离级别三者严丝合缝。少一个,就可能多扣一笔钱或多冻一单库存。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











