db.Exec单条写入撑不住高并发,因尖刺流量耗尽连接池、加剧锁争抢、堆积WAL;必须改用channel+定时/定量触发的轻量缓冲层批量入库,不依赖Kafka即可稳定支撑每秒数千写入。

db.Exec 单条写入撑不住高并发,不是数据库不行,是写法把连接池、WAL、事务开销全打满了。必须切到批量 + 缓冲队列模式,不引入 Kafka 也能稳住每秒数千写入。
为什么 goroutine 里直接 db.Exec 落库会越来越慢
看似并发高,实则制造“尖刺流量”:大量 goroutine 同时抢连接、争锁、刷 WAL,结果是连接池瞬间耗尽、事务排队、磁盘 I/O 堆积。too many clients already 或 Too many connections 不是配置低了,是没缓冲。
-
db.Exec单次平均耗时 5–20ms(含网络、解析、刷盘),而往chan发送只花纳秒级 - 连接池大小通常设为
runtime.NumCPU() * 2~4,远小于活跃 goroutine 数,大量协程卡在等待空闲连接上 - 高频小事务触发 WAL 写放大,SSD 场景下延迟飙升、耐久下降
- 哪怕每个 goroutine 只塞 10 条再
db.Exec,也因连接复用率低、SQL 解析重复,照样压垮
用 chan + 定时/定量触发做轻量缓冲层
不用消息中间件,靠原生 chan 和 select 就能搭出可控缓冲。核心就两条触发条件:攒够数量,或超时。
- 声明带缓冲 channel:
writeCh := make(chan writeOp, 1024)—— 太大易 OOM,太小易丢写(比如峰值每秒 2000 条,缓冲至少设 3000) - worker goroutine 用
select等待:case op := 收集,<code>case 超时兜底 - 触发后调用批量插入函数,别在循环里反复
db.Exec - 监控积压:
len(writeCh)超阈值(比如 800)就该告警或拒绝新写入(返回ErrQueueFull)
批量 SQL 怎么拼才安全又高效
别字符串拼接再 db.Exec,要用参数绑定防注入,同时适配不同数据库占位符规则。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- MySQL/SQLite 用
?占位:INSERT INTO users (id, name) VALUES (?, ?), (?, ?) - PostgreSQL 必须用
$1,$2,且values参数顺序必须严格对应,错一位就报ERROR: there is no parameter $2 - 单批别超 1000 行:MySQL 可能触发
max_allowed_packet,PG 可能被statement_timeout中断 - 失败时别
panic,记日志 + 保留这批数据重试;连续失败需加退避(如指数增长time.Sleep)
要不要用 GORM 的 CreateInBatches?
它底层确实生成单条多值 INSERT,比循环 Create 快 10 倍以上,但有硬伤:
- 默认不开启事务,某批失败时前面批次已提交 → 数据不一致,必须手动包
tx - 主键回填不可靠:MySQL 不支持
RETURNING,插入后users[0].ID仍是 0;PG 仅单批可用,多批时clause.Returning结果错位 - 当单次写入超 10 万行、或要求毫秒级延迟、或需数据库特有语法(
ON CONFLICT、LOAD DATA INFILE)时,GORM 抽象层反而成瓶颈,应直连*sql.DB
真正容易被忽略的是缓冲层的“呼吸感”:channel 关闭时机、worker panic 后是否自动重启、失败批次重试是否带 context deadline、以及每批操作是否单独设超时——这些细节不处理,压测时往往前 5 分钟正常,第 6 分钟开始缓慢堆积,最后雪崩。

















