直接INSERT会拖垮数据库,因其产生尖刺流量导致连接数暴涨、锁争抢和I/O堆积;应改用批量插入、内存缓冲或消息队列分层优化。

为什么直接 INSERT 会拖垮数据库
Go 程序里每来一个请求就开个 goroutine 执行一条 INSERT,看着并发高,实则在给数据库喂“尖刺流量”:连接数暴涨、事务锁争抢、磁盘 I/O 队列堆积。PostgreSQL 的 too many clients already 或 MySQL 的 Too many connections 不是配置调小了,是写法没缓冲。
- 单条
INSERT平均耗时 5–20ms(含网络+解析+刷盘),而 Go 写入 channel 或内存 buffer 只需纳秒级 - 数据库连接池大小通常设为 CPU 核数 × 2~4,远小于活跃 goroutine 数,大量协程卡在
db.Exec上等待空闲连接 - 高频小事务触发 WAL 写放大,尤其在 SSD 耐久度和延迟敏感场景下更明显
用 sql.DB 自带批量能力做最小改动
不引入新组件,先榨干标准库——database/sql 支持多值 INSERT,但得手拼 SQL 或用第三方扩展。原生最稳的方式是复用 Exec 的参数绑定机制:
// 示例:批量插入 100 条用户
values := make([]interface{}, 0, 300)
placeholders := make([]string, 0, 100)
for _, u := range users {
values = append(values, u.Name, u.Email, u.CreatedAt)
placeholders = append(placeholders, "(?, ?, ?)")
}
sql := "INSERT INTO users (name, email, created_at) VALUES " + strings.Join(placeholders, ", ")
_, err := db.Exec(sql, values...)- MySQL/SQLite 支持
?占位符;PostgreSQL 要改用$1, $2,且values...传参顺序必须严格对应 - 单次批量别超 1000 行:太大易触发 MySQL
max_allowed_packet或 PG 的statement_timeout - 别在循环里反复
db.Exec,哪怕每次只塞 10 条——连接复用率低,照样压垮连接池
用 chan + 定时/定量触发做内存缓冲层
自己搭轻量缓冲,核心就两条:用无缓冲或小缓冲 channel 接收写请求,另起 goroutine 按时间或数量攒批落库。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
type BatchWriter struct {
ch chan *User
done chan struct{}
}
func (w *BatchWriter) Start() {
ticker := time.NewTicker(100 * time.Millisecond)
batch := make([]*User, 0, 100)
for {
select {
case u := <-w.ch:
batch = append(batch, u)
if len(batch) >= 100 {
w.flush(batch)
batch = batch[:0]
}
case <-ticker.C:
if len(batch) > 0 {
w.flush(batch)
batch = batch[:0]
}
case <-w.done:
if len(batch) > 0 {
w.flush(batch)
}
return
}
}
}- channel 容量建议设为 10~50:太大内存积压,太小丢消息(除非你用带缓冲的 channel 并配好
select default降级) -
flush函数里务必用同一个*sql.Tx包裹整批写入,避免每条都开事务——否则锁粒度从行级升到表级 - 别忘了加
context.WithTimeout控制单次flush最长耗时,防止某批数据卡死整个管道
什么时候该切到消息队列(如 RabbitMQ 或 Kafka)
当出现以下任一情况,说明内存缓冲已不够用:chan 开始频繁阻塞、OOM 报警、写入延迟毛刺超过 1s、需要跨服务/跨机房写入、或要求严格有序与重试保障。
立即学习“go语言免费学习笔记(深入)”;
- Go 侧只需把
User序列化成 JSON 发到队列,不用管下游消费失败——由 MQ 的 ACK 机制兜底 - 避免在 Go 进程里直连 Kafka:用
segmentio/kafka-go而非confluent-kafka-go(后者 CGO 依赖易引发交叉编译问题) - RabbitMQ 场景下,别用默认的
auto-ack:Go 消费端处理完再显式ack,否则进程崩溃会导致消息丢失 - 注意序列化成本:
json.Marshal比gob慢 3–5 倍,但跨语言兼容性差;若纯 Go 生态,gob+binary更省 CPU
缓冲层级越深,排查链路越长。线上看到写入延迟升高,先查 chan 长度是否持续 >80%,再看 MQ 消费 lag,最后才碰数据库慢日志——顺序错了,一半时间花在冤枉路上。

















