database/sql.Tx 不能跨服务使用,因其仅绑定单个数据库连接,无法感知HTTP/gRPC、Redis或另一MySQL实例的事务状态;本地Outbox表通过同一事务保障业务修改与事件发布的原子性,是稳当起点。

database/sql.Tx 不能跨服务用,这不是框架缺陷,而是设计边界——它只绑定单个数据库连接,对 HTTP/gRPC 调用、Redis 操作、另一个 MySQL 实例的事务状态完全无感。你调用 tx.Commit() 成功,不代表下游服务也成功;defer tx.Rollback() 更不会让已发出的 HTTP 请求撤回。
为什么本地事务表(Outbox)是最稳的起点
它把“改业务数据”和“发事件”塞进同一个 db.BeginTx(),靠数据库 ACID 保原子性,不依赖协调器、不挑战 Go 生态缺失的 2PC 实现能力。
- outbox 表必须和业务表在同一个数据库实例、同一个事务中;跨库或跨实例就失效
-
INSERT INTO outbox前建议先SELECT ... FOR UPDATE锁住对应业务行,否则高并发下可能重复发事件 - 轮询 outbox 的 goroutine 要控频(如 100ms 间隔),太密打爆 DB,太慢拖长延迟
- 下游消费端必须幂等:用
INSERT IGNORE、UPSERT,或 HTTPIdempotency-Key+ 数据库唯一索引校验
Saga 模式下补偿逻辑总漏执行?重点查三件事
协同式 Saga 容易因事件丢失或消费者 crash 导致后续步骤不触发;编排式 Saga 则依赖 Orchestration 服务的可靠性。Temporal.io 是目前 Go 生态最接近“开箱即用”的选择,但它不是魔法——你仍需显式定义每步的 Execute 和 Compensate 函数。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
-
Compensate函数必须带context.Context并设超时,防止卡死阻塞整个流程 - 别用内存变量或
context.WithValue传状态;Temporal 的workflow ID + run ID才是唯一上下文 - 如果不用 Temporal,自己实现编排器时,务必把每步状态(
executing/compensating)落库,否则服务重启后无法续跑 - 正向操作与补偿操作共用同一笔
order_id,并在saga_records表中持久化每步状态(pending/succeeded/compensated)
TCC 的 Confirm/Cancel 总 panic 或资损?根源不在框架
典型错误是 panic: call to Confirm after Try failed 或余额多加一次,问题出在没管住三件事:状态校验、超时控制、日志表主键设计。
-
Confirm()开头必须SELECT ... FOR UPDATE WHERE status = 'trying',否则并发调用会误操作已Cancel的记录 -
tx_log表主键别用uuid,高并发INSERT会成热点;推荐(tx_id, branch_id)联合主键 + 时间戳字段做分片依据 -
Confirm和Cancel的 SQL 都要加context.WithTimeout,并透传到底层db.ExecContext(),否则锁等待卡住整个事务链路 - 别用
time.Now().Before(expireTime)判断 TCC 超时——节点间毫秒级时钟漂移就会导致Cancel被跳过;统一用 NTP 同步后的单调时钟
context.WithTimeout 只杀 goroutine,下游服务还在执行,就变成“幽灵事务”。

















