本地消息表必须与业务表同库以保证事务原子性;go-zero中需共用*sql.DB实例且禁用MyISAM;status宜用TINYINT;消息发送须“扫描+更新+发送”三步事务内完成,并用指数退避重试;幂等优先用DB唯一索引或条件更新。

本地消息表为什么必须和业务表在同一个数据库
因为要靠单机事务保证「业务操作 + 消息落库」原子性。如果消息表在另一个库,哪怕只差毫秒,就可能出现订单创建成功但消息丢失——下游永远收不到扣库存指令。
-
go-zero 里
message表必须和order表共用一个*sql.DB实例,不能跨db.Open() - 建表时别加
ENGINE=InnoDB外的引擎(比如 MyISAM),否则SELECT ... FOR UPDATE会失效 - 字段
status建议用TINYINT而非字符串,避免WHERE status = 'sent'索引失效
消息发送器怎么避免重复推送又不漏推
核心是「扫描 + 更新 + 发送」三步必须在一个事务里完成,并且更新条件带状态过滤。否则并发扫描时两个 goroutine 可能同时捞到同一条未发送消息。
- 用
UPDATE message SET status = 1 WHERE id = ? AND status = 0,检查RowsAffected是否为 1 再发 MQ - 扫描间隔别设成固定 1s,改用指数退避:第一次失败后等
time.Second * 2,再失败翻倍,上限 30s - 别在循环里直接调
kafka.Producer.SendMessage(),先用asynq.NewTask("send_to_kafka", ...)推进异步队列,防止网络抖动卡死整个扫描协程
消费者幂等处理最省事的三种写法
不是所有业务都值得为幂等搞分布式锁。多数场景下,用数据库唯一约束或条件更新就能拦住 95% 的重复消费。
- 库存服务收到
OrderPaidEvent后,执行UPDATE inventory SET stock = stock - ? WHERE sku_id = ? AND version = ?,失败就说明已处理过 - 给
order_id和event_type建联合唯一索引,插入前先INSERT IGNORE INTO event_log (...) VALUES (...) - 用 Redis
SETNX order:123:paid true EX 3600做轻量标记,注意别忘了设置过期时间,否则宕机后永久卡住
重试机制里最容易被忽略的 timeout 和 max_retry
很多人只加了 for i := 0; i ,但没控制每次重试的超时,结果一次 Kafka 网络卡顿就拖满 30 秒,压垮整个补偿服务。
立即学习“go语言免费学习笔记(深入)”;
- 每次调用
inventorySvc.Deduct()必须包一层ctx, cancel := context.WithTimeout(ctx, 3*time.Second) -
max_retry别硬编码,从配置中心读取,方便线上动态调低(比如大促时临时设为 1) - 第 3 次失败后,别再自动重试,改写入
compensation_failed表并触发告警,人工介入比盲目重试更可靠
本地消息表看着简单,但真正上线后出问题的,八成卡在「扫描事务没加 for update」「消费者没判影响行数」「重试没设单次超时」这三处。细节不在代码多漂亮,而在每一步是否明确知道它失败了会怎样。










