直接db.exec单条写入必然拖垮数据库,因其产生尖刺流量导致连接池占满、锁争抢和wal堆积;应改用channel+定时/定量触发的轻量缓冲层实现批量入库,不依赖消息中间件即可支撑每秒数千写入。

直接 db.Exec 单条写入高并发场景下必然拖垮数据库,必须用缓冲队列 + 批量入库组合方案;不引入消息中间件也能稳住每秒数千写入。
为什么不能在 goroutine 里直接 db.Exec
每个 HTTP 请求起一个 goroutine 调 db.Exec 看似并发高,实则制造“尖刺流量”:连接池瞬间占满、事务锁争抢、WAL 日志堆积。常见错误如 too many clients already(PostgreSQL)或 Too many connections(MySQL)不是配置低了,是写法没缓冲。
-
db.Exec单次耗时通常 5–20ms(含网络、解析、刷盘),而往chan发送只花纳秒级 - 数据库连接池大小一般设为 CPU 核数 × 2~4,远小于活跃 goroutine 数,大量协程卡在等待空闲连接上
- 高频小事务触发 WAL 写放大,SSD 耐久和延迟敏感场景下问题更突出
用 channel + 定时/定量触发实现轻量缓冲层
不用引入 Kafka 或 Redis,靠原生 chan 和 select 就能搭出可控的写入缓冲。核心是两个触发条件:攒够数量,或超时。
- 定义写入结构体:
type writeOp struct{ id int; name string; email string } - 声明带缓冲 channel:
writeCh := make(chan writeOp, 1024),太大易 OOM,太小易丢写 - worker goroutine 用
select等待:case op := 收集,<code>case 超时兜底 - 触发条件满足后,调用批量插入函数——别在循环里反复
db.Exec,哪怕每次只塞 10 条 - 监控积压:
len(writeCh)超阈值(比如 800)就该告警或拒绝新写入
批量插入 SQL 怎么写才安全高效
别拼接字符串再 sql.DB.Exec,要用参数绑定防注入,同时适配不同数据库占位符规则。
- MySQL/SQLite 用
?占位:INSERT INTO users (id, name, email) VALUES (?, ?, ?), (?, ?, ?) - PostgreSQL 必须用
$1, $2, $3,且values参数顺序必须严格对应,错一位就报ERROR: there is no parameter $2 - 单批别超 1000 行:MySQL 可能触发
max_allowed_packet,PG 可能被statement_timeout中断 - 失败时别 panic,记日志 + 保留这批数据重试;连续失败需加退避(如指数增长 delay)
- 用
INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或UPSERT(PG)替代先查后插,避免竞态
Write-Behind 场景下必须处理的三个陷阱
缓存写入不是把数据扔进 map 就完事,顺序、丢写、刷盘时机才是关键。
- 别用
sync.Map+time.AfterFunc做高频写入——timer 泄漏、合并粒度不可控、延迟抖动大 - 重启丢失:纯内存缓冲无持久化,若要求不丢,得落地本地磁盘队列(如用
os.File追加写 + 定期 rotate) - 重复写覆盖:同一 key 多次变更,只应落库最后一次;用
map[key]struct{ value string; ts int64 }记时间戳比单纯Store更可靠 - 无事务:Write-Behind 本质是“尽力而为”,关键业务(如支付扣款)仍需同步强一致写入
最易被忽略的是刷盘失败后的状态恢复逻辑——缓存中已删但 DB 没写成的数据,必须保留在内存或落盘队列里,等下次 flush 重试;否则看似跑通,其实悄悄丢数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











