真正可用的异步写入必须带缓冲、单写协程、批量落盘三要素;直接 go db.Exec 或 go log.Print 仅转移阻塞,易致连接池耗尽、日志错乱、OOM 等问题。

直接用 go db.Exec 或 go log.Print 不算异步写入,只是把阻塞从主线程转移到 goroutine,反而制造连接池耗尽、日志错乱、OOM 等新问题。真正可用的异步写入必须带缓冲、单写协程、批量落盘三要素。
为什么不能直接 go db.Exec 或 go file.WriteString
常见错误是每个请求起一个 goroutine 执行 db.Exec 或 file.WriteString,看似“并发”,实则埋雷:
-
db.Exec每次调用都占一个连接,连接池(通常设为 CPU×2~4)远小于活跃 goroutine 数,大量协程卡在waiting for available connection -
file.WriteString是同步系统调用,多个 goroutine 并发写同一文件,在 ext4/xfs 上可能因内核页缓存导致日志行粘连或截断 - 无缓冲 channel 或无上限内存堆积,突发流量把几千条日志/记录全塞进内存,GC 压力飙升甚至 OOM
- 进程退出时,未 flush 的 bufio 缓冲、未 sync 的文件数据、未提交的事务全部丢失
最小可行异步写入结构(纯 stdlib)
不依赖第三方库也能跑通,核心就四行代码逻辑 + 两个控制点:
- 声明带缓冲 channel:
writeCh := make(chan *WriteItem, 1024),容量按压测峰值 ×3~5 设(如 800 条/秒 → 4096) - 启动**唯一**写协程:
go writeLoop(writeCh, db)或go fileWriter(writeCh, file),禁止多个协程并发写同一资源 - 用
bufio.Writer批量攒写:w := bufio.NewWriterSize(file, 16*1024),显式设 buffer 大小,避免默认 4KB 太小 - 关键数据必须
file.Sync()落盘,别只靠w.Flush();数据库批量插入后要tx.Commit(),失败需重试+退避
channel 写入时怎么防丢、防卡、防 panic
生产环境最常崩在这三处,不是逻辑错,而是保护没做:
立即学习“go语言免费学习笔记(深入)”;
- 写入前检查水位:
if len(writeCh) > cap(writeCh)*0.8 { dropOrSync(item) },超阈值降级为同步写或丢弃非关键字段 - 消费者必须 recover:
defer func() { if r := recover(); r != nil { log.Error(r) } }(),否则 panic 后整个 channel 积压,进程退出全丢 - 加超时取值:
select { case item := ,防底层 I/O 卡死拖垮整个协程 - 进程退出前等待落盘:
close(writeCh); time.Sleep(2 * time.Second),但别无限等,2 秒是经验值
批量 SQL 和日志序列化的坑
批量操作看似简单,参数和格式稍错就报错或数据错位:
- MySQL/SQLite 占位符统一用
?,values 参数顺序必须严格对应 SQL 中?出现顺序,错一位就报ERROR: there is no parameter $2 - 单批别超 1000 行:MySQL 可能触发
max_allowed_packet,PostgreSQL 可能被statement_timeout中断 - 日志序列化优先用
json.Marshal(易调试),性能敏感场景可换gob或拼接字符串,但要注意time.Time字段需提前转Format - 审计日志若要求强持久化,
file.Sync()必须加,且建议每批 flush 后 sync,别攒太久
真正难的不是写出来,而是水位监控、崩溃恢复、降级开关这三件事没人写——它们不出问题时看不见,一出就是线上事故。


















