不能在 HTTP handler 里直接调用补偿逻辑,因为会阻塞主链路、引发超时与雪崩;必须异步化推送至队列,并配重试与幂等机制,结合本地事务表保证原子性,对账修复作为兜底手段。

为什么不能在 HTTP handler 里直接调用补偿逻辑
因为主链路会卡死、超时、雪崩——你本想“修复不一致”,结果先搞垮了下单接口。Go 的 http.Handler 是短生命周期的,没做重试兜底、没设上下文超时、没隔离失败传播,一旦库存服务响应慢或返回 503,整个请求就 hang 在那里,goroutine 积压,QPS 断崖下跌。
- 所有补偿任务必须异步化,用
github.com/hibiken/asynq或asynq.Client推送进队列,handler 只负责“发消息” - 补偿任务本身要带明确的
max_retry = 3和指数退避(如time.Second * 1、time.Second * 2、time.Second * 4) - 不要用
time.AfterFunc做延迟补偿——进程一挂就丢,必须依赖持久化调度器(如asynq的ProcessAt或entgo + cron job)
如何保证消息发送和本地事务的原子性
这是最终一致性最常翻车的地方:订单写成功了,但“扣库存”事件没发出去;或者事件发出去了,订单却因 panic 回滚了——两边彻底失联。
- 别用“先发消息再 commit”或“先 commit 再发消息”,这两种都不可靠
- 正确做法是“本地事务表 + 定时扫描”:在订单服务 DB 里建一张
outbox_events表,字段含event_type、payload、processed;事务内先 insert event 再 update order,两者在同一个tx.Commit()下 - 另起一个独立 goroutine(或定时 job),轮询
SELECT * FROM outbox_events WHERE processed = false LIMIT 100,成功发送后UPDATE outbox_events SET processed = true
补偿函数为什么必须幂等,以及怎么写才真正幂等
消息队列不保证“只投递一次”,Kafka 可能重复,RabbitMQ 手动 nack 会重入,网络抖动也可能触发重试——如果你的 restoreInventory() 每次都加回 10 件,三次调用就多出 20 件库存。
- 幂等不是“加个 if 判断”,而是基于状态机或唯一约束:比如在
inventory_compensation_log表中用UNIQUE (order_id, action)索引,插入前先INSERT IGNORE,失败即说明已执行过 - 更新操作要用条件 where,例如
UPDATE inventory SET stock = stock + ? WHERE sku_id = ? AND status = 'frozen',避免对已恢复的记录二次加回 - 不要依赖内存缓存或临时 map 记录执行状态——服务重启就清零,等于没做幂等
什么时候该放弃“立刻补偿”,改用对账修复
不是所有不一致都要秒级修复。比如积分发放失败,用户根本感知不到;物流单号同步延迟 30 秒,不影响发货。强行实时补偿反而增加系统负担、掩盖真实瓶颈。
立即学习“go语言免费学习笔记(深入)”;
- 对账服务应独立部署,周期性扫描(如每分钟)关键状态不匹配的记录,例如
SELECT * FROM orders WHERE status = 'paid' AND inventory_updated = false - 对账任务也要走异步队列,且需打标:同一笔订单的对账任务用相同
queue_name+task_id,配合 RedisSETNX防重入 - 对账发现异常后,优先记录日志并上报指标(如
inventory_reconcile_mismatch_count),而不是立刻重试——先看清模式,再决定是否人工介入
最容易被忽略的是状态落地时机:所有中间态——事务 ID、当前步骤、补偿是否触发、重试次数——必须写库,不能存在内存、channel 或 context.Value 里。服务重启不是异常,是常态;没落库的状态,就是注定丢失的一致性。










