Go循环中使用Goroutine执行数据库插入是否为最佳实践?
先纠正一个关键问题:你测试得到的427ns耗时完全不反映真实的数据库插入耗时——你的代码只是启动了所有协程就结束计时了,根本没等待所有数据库插入操作完成。这个时间只是创建并启动协程的开销,不是真正的业务执行时间,实际所有插入任务可能还在后台跑,你根本没统计到。
下面说为什么这种方式不是最佳实践,以及正确的思路:
核心问题
无等待的异步任务导致统计失真
你没有用sync.WaitGroup这类工具等待所有协程执行完毕,所以计时结果毫无参考价值,甚至会误导你以为并发带来了巨大性能提升,实际上只是没等任务做完。数据库资源过载风险
如果save_in_db每次创建新连接,大量协程同时发起连接请求会直接打满数据库的连接数上限,触发报错;即使复用连接池,过多并发插入会导致数据库内部锁竞争加剧、IO瓶颈凸显,反而比串行插入更慢,甚至出现超时、死锁。错误完全不可控
你的代码没有处理save_in_db可能出现的任何错误(比如主键冲突、数据库连接失败),协程内的错误或panic会被静默忽略,你根本不知道哪些数据插入失败,排查问题无从下手。
正确的批量插入方案
方案1:优先用数据库原生批量插入(最优)
对于少量数据(比如你说的5条),直接用数据库的批量插入语法,把所有数据拼成一条SQL执行,这是性能最高、最可靠的方式:
// 准备批量DTO var dtos []product_dto for _, j := range items { dtos = append(dtos, prepare_dto(j)) } // 执行批量插入,比如MySQL的语法:INSERT INTO table (...) VALUES (...), (...), ... err := batch_save_in_db(dtos) if err != nil { // 统一处理错误 }
这种方式只需要一次数据库请求,避免了多次连接开销,数据库内部也能高效处理批量写入,性能远优于开多个协程。
方案2:并发插入(仅适用于超大量数据)
如果数据量极大,单条批量SQL过长(超过数据库限制),可以用带并发限制的协程执行插入,同时做好等待和错误处理:
import ( "sync" "log" "fmt" ) func batchInsert(items []product_object) error { var wg sync.WaitGroup // 限制并发数,比如最多10个协程同时执行,根据数据库连接池大小调整 semaphore := make(chan struct{}, 10) errChan := make(chan error, len(items)) for _, j := range items { wg.Add(1) semaphore <- struct{}{} // 获取并发许可 go func(item product_object) { defer wg.Done() defer func() { <-semaphore }() // 释放并发许可 obj := prepare_dto(item) if err := save_in_db(obj); err != nil { errChan <- err log.Printf("插入产品ID:%d失败: %v", item.id, err) } }(j) } // 等待所有协程完成 wg.Wait() close(errChan) // 收集错误 var errs []error for err := range errChan { errs = append(errs, err) } if len(errs) > 0 { return fmt.Errorf("共%d条插入失败: %v", len(errs), errs) } return nil }
这种方式既利用了并发提升效率,又避免了无限制并发压垮数据库,同时能收集所有错误。
总结
- 数据量小(几十/几百条):直接用批量插入,不要用协程,简单高效。
- 数据量极大:用带并发限制的协程+等待机制+错误收集,同时根据数据库连接池大小调整并发数。
- 绝对不要像你原来那样直接开N个协程就不管了,既无法统计真实耗时,又会带来各种稳定性问题。
内容的提问来源于stack exchange,提问作者Sowmya Ravichandran

