队列长度过小会导致任务丢弃或阻塞上游;应按峰值QPS×平均处理耗时×安全系数1.5估算缓冲容量,避免依赖len(ch)实时判断,须配非阻塞写入逻辑。

队列长度设太小会导致任务丢弃或阻塞上游
用 chan Task 做任务队列时,缓冲为 0 就是无缓冲 channel,生产者会直接阻塞直到消费者就绪——这在高并发写入场景下极易卡死 API 入口。缓冲过小(比如只设 10)则稍有流量毛刺就填满,后续 select + default 会直接丢任务,或 len(chan) 检查后误判容量不足而拒绝请求。
- 典型做法:按「峰值 QPS × 平均处理耗时 × 安全系数 1.5」估算,例如每秒 800 请求、平均处理 120ms → 800 × 0.12 × 1.5 ≈ 144,取整设为
make(chan Task, 200) - 若任务处理耗时波动大(如含外部 HTTP 调用),需拉长窗口统计 P95 耗时,再代入公式
- 别依赖
len(ch)实时判断是否可写——它只是快照值,多 goroutine 并发读写时已失效
用带缓冲 channel 时必须配非阻塞写入逻辑
即使设置了合理缓冲,也不能直接 ch ,否则一旦队列满,goroutine 会永久挂起。必须用 <code>select + default 显式控制背压行为。
- 正确写法:
select { case ch <- task: // 写入成功 default: // 队列满,走降级:打日志、返回 429、丢弃或转异步落盘 } - 不要用
if len(ch) —— 这中间存在竞态,两个 goroutine 同时通过检查,第二个必然 panic 或阻塞 - 如果业务允许延迟,可改用带超时的
select:case ch 和 <code>case
全局队列(如 Redis List)要防堆积而非只看长度
用 LLEN 查 Redis 队列长度做准入控制,本质是竞态操作:多个请求同时读到 999,都判定“还能进”,结果变成 1001+。这不是配置问题,而是设计缺陷。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须用 Lua 脚本原子完成「入队 + 截断 + 返回当前长度」:
local len = redis.call('LPUSH', KEYS[1], ARGV[1]) redis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[2]) - 1) return len - 其中
ARGV[2]是硬上限(如 1000),LTRIM确保内存不爆炸,哪怕瞬时涌入 10 万条也只留最新 1000 - 别用
INCR+EXPIRE组合做限流计数——同样有竞态;改用EVAL包裹,或 Valkey 7.0+ 的INCRBYEX(但 Go 客户端支持不一)
协程池队列溢出比 channel 更隐蔽
像 ants 或手写 worker pool 的任务队列,常默认用无界 channel 或 slice,表面不报错,实则内存持续上涨,GC 压力陡增,最后 OOM。
立即学习“go语言免费学习笔记(深入)”;
- 启用有界队列:如
ants.NewPool(100, ants.WithNonblocking(true), ants.WithMaxBlockingTasks(1000)),超限时自动拒绝 - 监控指标不能只看 goroutine 数,要抓
pool.RunningWorkers()和pool.FreeWorkers(),发现长期FreeWorkers == 0且队列积压,说明 worker 数或单任务耗时不合理 - 手写池时,
taskChan必须设缓冲,且关闭逻辑要确保close(taskChan)后所有 worker 已 drain 完,否则range会漏任务
实际最难的不是算出那个数字,而是确认「这个长度在最差网络延迟+最长 DB 查询+突发流量下是否仍能稳住」——得靠线上真实压测,而不是公式。

















