高并发队列需控制消息流向、执行边界和失败兜底;推荐按Key分桶+单goroutine串行消费,用fnv.New64a哈希取模分桶,sync.Map存储worker,避免全局channel串行瓶颈。

高并发队列在 Go 里不是靠“堆 goroutine”或“加大 buffer”就能稳住的,关键在于控制三个东西:消息流向、执行边界、失败兜底。盲目用 go f() 或无缓冲 chan,线上扛不过 5 分钟就会出现 panic、积压、丢任务或 goroutine 泄漏。
按 Key 分桶 + 单 goroutine 串行消费
这是解决「部分有序 + 整体并发」最直接有效的方案,比如用户操作必须按顺序执行,但不同用户之间可并行。
- 用
fnv.New64a()或自定义哈希函数对业务 key(如user_id、order_no)计算 hash,再& (N-1)取模分桶(N=256或1024) - 每个桶绑定一个专属
worker:启动时go func() { for msg := range ch { process(msg) } }() - 用
sync.Map存map[uint64]*worker,首次访问 key 时原子注册,避免 map 并发写 panic - 切忌把所有消息塞进同一个 channel —— 这等于用一把锁串行所有流量
带缓冲 channel + select default 非阻塞入队
缓冲大小不是拍脑袋定的,它本质是背压策略的体现:太小易丢,太大掩盖瓶颈。
- 初始设
make(chan Task, 100),上线后监控len(ch) > 0.8*cap(ch)触发告警 - 生产者入队必须用
select { case ch ,不能裸写 <code>ch - buffer 满时别丢消息也不该 panic,要返回明确错误,由上游决定重试、降级或进死信
- 别用
default分支做日志+continue——这会静默丢任务,破坏顺序性
Worker 必须用 sync.WaitGroup + context 控制生命周期
只 close(ch) 不等 worker 处理完,等于拔电源前没关电脑——任务就卡在半路了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 每个 worker 启动前调
wg.Add(1),结尾defer wg.Done();主 goroutine 必须wg.Wait()才退出 - 每条消息配独立
ctx:比如ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second),防止某条 DB 查询卡死整个 worker - worker 内部加
defer func() { if r := recover(); r != nil { log.Printf("panic on %v: %v", msg.ID, r) } }(),panic 不扩散 - 优雅退出时,先 close input channel,再处理完剩余消息,最后 close(done) 通知管理器
别忽略可观测性与复用开销
高并发下最危险的不是慢,而是“看起来正常却悄悄丢数据”。很多问题直到积压爆发才暴露。
- 打关键日志:worker 启动、收到首条消息、处理完成、退出前;不要只在出错时打
- 用
sync.Pool复用任务结构体,高频场景下能显著降低 GC 压力,但注意 Pool 中对象不保证复用,也不能存跨生命周期状态 - 别共用一个
chan接收多个来源消息(Kafka/RabbitMQ/HTTP),必须在Message结构体里带Source string字段,靠字段分支处理,而非靠外部变量判断 - 缓冲区水位、单任务耗时 P99、panic 次数、死信量——这些指标比 QPS 更早预警系统失衡
真正难的不是写出让程序跑起来的队列,而是让队列在流量突增、下游抖动、worker panic、进程重启时,依然不丢不乱不卡。每一个 select 分支、每一处 defer、每一次 sync.Map.LoadOrStore,都是为这个目标服务的。

















