本地队列(P.runq)是Go调度器中每个P独有、无锁的FIFO队列,存取零同步开销且缓存友好;满256时触发半队列迁移至全局队列以实现负载均衡;仅在本地空、窃取失败、系统调用返回或GC后等特定路径下才访问全局队列。

为什么本地队列比直接开 goroutine 更稳
直接每个请求起一个 goroutine 执行 db.Exec,看似并发高,实则把数据库当成了“共享打印机”:连接池迅速耗尽、事务锁排队、WAL 日志写爆。而 Go 调度器的本地队列(P 的 runq)本身不参与数据库压力缓解——它只管调度 goroutine,不管你怎么用 DB。真正起缓冲作用的是你手动建的带缓冲 chan,它才是业务层的“本地队列”。别被术语绕晕:这里的“本地队列”不是 runtime 内部那个,是你代码里 make(chan *Task, 1000) 这个。
怎么用 channel 模拟轻量本地队列
核心就三点:有界缓冲、非阻塞入队、单消费者批量落库。这不是替代消息队列,而是最简可行缓冲层。
- 缓冲大小按峰值写入速率 × 1.2~1.5 预估,比如每秒 800 条写入 →
make(chan *User, 1200);设太大(如 10w)会吃光内存,太小(如 10)起不到缓冲作用 - HTTP handler 里必须用
select+default非阻塞写入:select { case taskChan ,否则上游一卡,整个服务线程全挂住 - 只启一个 goroutine 消费该 channel,攒够 100~500 条再
tx.Exec批量插入;多个 goroutine 并发消费同一 channel 写 DB,只会触发连接争抢和锁冲突,吞吐反而下降
批量插入时占位符和参数顺序不能错
MySQL/SQLite 用 ?,PostgreSQL 必须用 , ;且 values 切片顺序必须和 SQL 中占位符严格对应。错一位,sql.ErrNoRows 或 pq: invalid input syntax 就来了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 拼 SQL 时别手写
(?, ?, ?), (?, ?, ?),用strings.Repeat("(?, ?, ?),", n-1) + "(?, ?, ?)"避免逗号多写或少写 - PostgreSQL 下传参顺序错乱,错误信息是
pq: there is no parameter $3或更隐蔽的类型转换失败 - 单次批量别超 1000 行:MySQL 可能触发
max_allowed_packet,PG 可能因statement_timeout中断,且大事务回滚代价高
关闭 channel 时最容易丢数据
很多人在主 goroutine 关闭 taskChan 后,就直接 return,结果最后一批任务还在 channel 里没消费完。真正的安全关闭必须三步走:
立即学习“go语言免费学习笔记(深入)”;
- 先停投递:所有生产者不再往
taskChan发送新任务 - 再等消费:启动的 worker goroutine 用
for t := range taskChan持续取,直到 channel 关闭、循环自然退出 - 最后收尾:worker 退出后,主 goroutine 才能确认这批数据已落库;若需结果反馈,
resultChan也得等所有 worker 完成后再close
漏掉任一环,轻则日志里看到 send on closed channel panic,重则线上漏写几百条用户注册记录。

















