必须等数据库事务提交成功后再发Kafka消息,且需等待Broker全副本确认(sarama.WaitForAll),消费者端须在GORM事务Commit后才提交offset,并对可重试错误实现指数退避与死信投递。

事务提交前不能发Kafka消息
直接在 tx.Commit() 之前调用生产者发消息,是常见但危险的做法。数据库事务成功、Kafka却发送失败(网络抖动、Broker不可用、序列化错误),会导致状态不一致:DB已写入,下游却没收到通知。
更糟的是,如果事务回滚了,但消息已经发出去,消费者照样会处理一条“本不该存在”的事件——这是典型的“至少一次”语义失控。
- 必须等
tx.Commit()成功返回后,再触发 Kafka 发送 - 若
tx.Commit()失败,整个流程终止,Kafka 消息根本不该构造 - 不要把 Kafka 发送逻辑包进
defer或异步 goroutine,否则无法感知 commit 结果
发完Kafka后要等ACK才算事务闭环
只调用 producer.SendMessage() 不等于消息已落盘。sarama 的 SyncProducer 会阻塞等待 Broker 确认,但确认级别由 config.Producer.RequiredAcks 控制:sarama.WaitForAll 才真正代表 ISR 全部副本写入成功;设成 sarama.NoResponse 或 sarama.WaitForLocal 时,看起来“发成功”了,其实可能丢数据。
- 生产环境务必设
config.Producer.RequiredAcks = sarama.WaitForAll -
delivery.timeout.ms要大于 Kafka 端的replica.lag.time.max.ms,否则可能因副本同步慢而误判超时 - 若
SendMessage()返回 error,说明消息未被 Broker 接收,此时应记录告警并触发补偿(比如写本地失败表+定时重试)
消费者端:GORM事务和消息ACK必须原子绑定
Kafka 消费者没有内置 ACK 机制,sarama.ConsumerGroup 中的 offset 提交是独立动作。常见错误是业务逻辑用 GORM 写库成功了,但忘记调用 session.MarkMessage(message, ""),导致重启后重复消费;或者先 MarkMessage 再写 DB,DB 失败了,offset 却已提交。
- 必须在
ConsumeClaim内、GORM 事务Commit()成功之后,才调用session.MarkMessage() - 若事务
Rollback(),不要 MarkMessage,让 offset 停留在原位,下次重拉 - 避免在
ConsumeClaim外做任何耗时操作(如 HTTP 调用、文件读写),否则会卡住整个分区消费协程
死信与重试不能只靠Kafka自身
Kafka 本身不提供 NACK 或重试队列。当 GORM 事务因唯一约束冲突、外键失败、网络超时等反复报错,你不能指望 Kafka 自动把消息“退回重试”。必须自己实现:
- 捕获具体 error 类型(如
*mysql.MySQLError、context.DeadlineExceeded),区分可重试(临时性)和不可重试(业务逻辑错误) - 对可重试错误,用指数退避 + 最大重试次数控制,失败后投递到专用死信 Topic(如
user_events_dlq) - 死信 Topic 需单独配置 retention 时间,并配套监控告警——没人看的 DLQ 和没建一样
最易忽略的一点:Kafka 的 version 和 timeout 参数不报错,但一旦配错(比如用 v3.0 客户端连 v2.8 Broker 且未降级),消息会静默卡在 pending 状态,GORM 事务早已结束,而消费者线程还在等一个永远不会来的响应。



















