跨服务异步任务必须用消息队列而非goroutine直调HTTP;事件需带版本号、精简字段;消费者必须幂等;内部异步用限流worker pool。

用消息队列发事件,别用 goroutine 直接调 HTTP
微服务间异步任务的首要原则:**跨服务通信必须走消息队列,不能靠 go http.Post 这类伪异步**。它看起来快,实则一断就丢、无重试、不幂等、不可观测——订单创建后发短信失败?用户收不到,你还查不到日志。
- RabbitMQ 适合强可靠性场景(如金融扣款通知),用
streadway/amqp库时务必设DeliveryMode: amqp.Persistent,否则重启 Broker 消息全丢 - NATS JetStream 更轻量,
nats-io/nats.go默认支持 At-Least-Once 投递,但消费端必须手动调msg.Ack()或msg.NakWithDelay(),否则消息会无限重发 - Kafka 吞吐高,但单条延迟不稳定;
segmentio/kafka-go的WriteMessages是批量写入,若只发一条,记得加Flush()或设小BatchTimeout
定义事件结构体时,版本号和字段精简比序列化方式更重要
事件是服务间的唯一契约,不是临时传参。JSON 虽通用,但结构混乱会导致下游解析 panic;而 encoding/gob 虽快,却锁死语言生态——一旦 Java 服务要接入,就只能重写生产者。
- 每个事件 struct 必须带
Version string `json:"version"`字段,例如"v1",后续兼容升级靠它分流处理逻辑 - 只传必要字段:比如
OrderShippedEvent里放OrderID、Status、ShippedAt就够了,别塞整个Order对象 - 避免嵌套 map 或 interface{}:Go 的
json.Unmarshal遇到未知字段会静默忽略,不如用明确 struct 字段 +json:"required"标签配合校验
消费端不幂等,等于没做异步
消息队列不保证“只投一次”,网络抖动、消费者崩溃、超时重试都会导致重复消息。你写的 handler 如果直接更新数据库或调第三方 API,就会出现双扣库存、发两封邮件这类线上事故。
- 最稳的做法:用业务主键(如
order_id)+ 状态表做INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或REPLACE INTO(MySQL) - 轻量方案可用 Redis:
SETNX processed:order_12345 true EX 3600,失败直接 return,别硬扛 - 切忌在 handler 里再发 HTTP 请求——这又绕回“伪异步”陷阱;应先落库,再由另一个服务监听状态变更事件去发
本地异步任务用 worker pool,别裸起 goroutine
服务内部的异步动作(如写审计日志、刷新缓存)可以不用消息队列,但也不能在 HTTP handler 里随手 go sendEmail()——goroutine 泛滥会吃光内存,panic 会杀死整个协程,且无法统计成功率。
立即学习“go语言免费学习笔记(深入)”;
- 建一个固定大小的 worker pool:
make(chan Task, 100)接任务,启动 4~8 个 goroutine 循环读取执行 - 每个 worker 必须包
defer func(){if r := recover(); r != nil { log.Printf("worker panic: %v", r) }}(),防止单个任务崩掉整个池 - 任务提交后不要等结果,但可选地把
Done chan error塞进Task结构体,供关键路径 select 超时监听
真正难的不是怎么发消息,而是怎么确保每条消息都被「恰当地」处理一次——从事件定义的稳定性,到消费者幂等的实现粒度,再到失败消息的可观测路径(DLQ + trace ID + Prometheus 指标),漏掉任何一环,异步就变成了“不确定”。










