根本原因是uploadstream非线程安全,多goroutine共用同一stream会导致元数据错乱、chunk重叠;必须为每个文件独占一个stream,且写入与close()由同一goroutine完成。

并发写入GridFS时为什么文件内容会损坏或丢失
根本原因是 gridfs.Bucket.UploadStream 返回的 io.WriteCloser 不是线程安全的,且内部缓冲和元数据提交(如 UploadStream.Close())必须由**同一个 goroutine 完成**。如果多个 goroutine 同时往同一个 stream 写、或提前 close,会导致 BSON 元数据错乱、chunk 写入重叠、甚至 context deadline exceeded 错误。
常见错误现象包括:
- 文件能上传但下载后大小为 0 或不完整
-
write: write tcp ... i/o timeout或context canceled频发 - MongoDB 中
fs.chunks出现重复files_id+n组合
正确做法是:每个文件独占一个 UploadStream,且整个写入+close 必须在单个 goroutine 内完成。
如何为每个文件创建独立、可并发的 UploadStream
关键不是“复用 bucket”,而是“每次调用 bucket.OpenUploadStream 获取新 stream”。gridfs.Bucket 本身是并发安全的,但返回的 stream 不是 —— 所以你要确保每个 stream 生命周期隔离。
实操建议:
- 不要把
UploadStream作为全局或共享变量传递 - 每个文件启动 goroutine 后,**立即调用
bucket.OpenUploadStream**,并在该 goroutine 内完成全部写入和Close() - 传入带超时的
context.Context,避免单个失败阻塞整体 - 用
errgroup.Group管理并发 goroutine 并统一等待/捕获错误
示例片段:
g, ctx := errgroup.WithContext(context.Background())
for _, file := range files {
file := file // 防止循环变量捕获
g.Go(func() error {
stream, err := bucket.OpenUploadStream(ctx, file.Name, options.GridFSUpload().SetChunkSizeBytes(256*1024))
if err != nil {
return fmt.Errorf("open upload stream for %s: %w", file.Name, err)
}
defer stream.Close() // 必须在此 goroutine 中 close
if _, err := io.Copy(stream, file.Reader); err != nil {
return fmt.Errorf("write to stream for %s: %w", file.Name, err)
}
return nil
})
}
if err := g.Wait(); err != nil {
log.Fatal(err)
}
为什么 SetChunkSizeBytes 要显式设置且不宜过大
默认 chunk 大小是 255KB,但 mongo-go-driver 在并发高吞吐场景下,过大的 chunk(如 1MB+)容易触发单次 write 超时或内存压力,尤其当网络抖动或 MongoDB 副本集同步延迟时。
参数影响说明:
- 太小(如 8KB)→
fs.chunks文档数量暴增,插入压力大,查询fs.files元数据变慢 - 太大(如 2MB)→ 单次
Write可能阻塞数秒,goroutine 协程长时间占用,context 更易超时 - 推荐值:256KB–512KB,平衡 chunk 数量与单次写入稳定性
注意:SetChunkSizeBytes 必须在 OpenUploadStream 时传入,无法在 stream 创建后修改。
上传完成后如何验证文件完整性(而非只看 error)
并发写入后,仅靠 err == nil 不能保证文件真正可用 —— fs.files 元数据可能已写入,但部分 chunk 还没落盘(尤其在 write concern 较弱时)。
可靠验证方式:
- 调用
stream.Close()后,用bucket.Find查fs.files确认length和uploadDate字段存在且合理 - 对关键文件,额外执行一次
bucket.OpenDownloadStream+io.Discard,确认能打开并读完(不耗内存) - 生产环境建议开启 write concern:在
OpenUploadStream的options.GridFSUpload()中加SetWriteConcern(writeconcern.Majority())
最容易被忽略的一点:GridFS 的 uploadDate 是服务端时间,若你依赖它做时效判断,需注意客户端和服务端时钟偏差 —— 别用它做精确并发顺序判定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











