MongoDB GridFS 一些处理细节解析

GridFS 不是 MongoDB 的独立存储引擎,而是运行在普通集合之上的一套文件分块协议。理解它,本质上是理解两张表、一个写入顺序约定、以及一套读取时的验证机制。


一、整体架构

GridFS 将每个文件分散存储在两个集合中:GridFS 还会自动创建两个关键索引。fs.files 上的 { filename: 1, uploadDate: 1 } 支持按文件名查最新版本;fs.chunks 上的 { files_id: 1, n: 1 } 唯一复合索引是整个机制的正确性基础------unique: true 防止同一块被重复写入,n 保证顺序扫描无需额外排序。


二、文件保存的完整处理逻辑

核心原则

chunks 全部写完,才写 files。 fs.files 记录的出现是文件"正式对外可见"的唯一信号,顺序不能颠倒。

写入流程### 缓冲区机制(C# 驱动核心逻辑)

C# 驱动的 Write() 不要求调用方一次传入整块数据,内部维护一个大小为 chunkSizeBytes_buffer,等积满才触发一次数据库写入:

csharp 复制代码
public override void Write(byte[] buffer, int offset, int count)
{
    while (count > 0)
    {
        // 计算本次能填入内部缓冲的字节数
        int available = _chunkSizeBytes - _bufferOffset;
        int toCopy    = Math.Min(count, available);

        Array.Copy(buffer, offset, _buffer, _bufferOffset, toCopy);
        _bufferOffset += toCopy;
        offset        += toCopy;
        count         -= toCopy;

        // 缓冲满 → 立即落盘一个 chunk
        if (_bufferOffset == _chunkSizeBytes)
            FlushCurrentChunk();  // 插入 fs.chunks,重置 _bufferOffset = 0
    }
}

private void FlushCurrentChunk()
{
    chunksCollection.InsertOne(new BsonDocument {
        { "files_id", _fileId       },
        { "n",        _chunkNumber++},
        { "data",     new BsonBinaryData(_buffer, 0, _bufferOffset) }
    });
    _bufferOffset = 0;
}

Close() 时处理尾块,再写 fs.files

csharp 复制代码
public override void Close()
{
    if (_bufferOffset > 0)          // 有剩余字节 → 写尾块(大小 < chunkSizeBytes)
        FlushCurrentChunk();
    // 若 length == 0,不创建任何 chunk

    filesCollection.InsertOne(new BsonDocument {
        { "_id",        _fileId         },
        { "filename",   _filename       },
        { "length",     _totalLength    },  // Int64,精确字节数
        { "chunkSize",  _chunkSizeBytes },  // Int32
        { "uploadDate", DateTime.UtcNow }
    });
}

分块规则

复制代码
文件长度:700 KB    chunkSize:255 KB

├─ n=0  [━━━━━━━━━━━━━━━  255 KB  ━━━━━━━━━━━━━━━]  满块,精确等于 chunkSize
├─ n=1  [━━━━━━━━━━━━━━━  255 KB  ━━━━━━━━━━━━━━━]  满块,精确等于 chunkSize
└─ n=2  [━━━━━━━━  190 KB  ━━━━━━━━]                 尾块,≤ chunkSize,不补零

总块数 = ⌈ 716800 / 261120 ⌉ = 3
  • 除最后一块外,所有块必须精确等于 chunkSizeBytes
  • 尾块只包含实际剩余字节,不补零
  • 空文件(length = 0)不创建任何 chunk 文档

孤儿 chunks 清理

上传中断会产生有 fs.chunks 记录但无 fs.files 记录的孤儿块,需定期清理:

javascript 复制代码
// 删除所有孤儿 chunks
db.fs.chunks.deleteMany({
  files_id: { $nin: db.fs.files.distinct("_id") }
})

三、文件获取逻辑

读取流程

读取是写入的镜像,先查元数据确认文件完整,再按索引顺序扫描分块,流式重组输出。### ProcessNextBatch() 三重验证

下载时驱动对每个 chunk 执行三层强制验证(C# 等效逻辑):

csharp 复制代码
void ProcessNextBatch(List<BsonDocument> batch)
{
    long lastN = CalculateTotalChunks() - 1;

    foreach (var chunk in batch)
    {
        int    n          = chunk["n"].AsInt32;
        byte[] data       = chunk["data"].AsByteArray;
        bool   isLastChunk = (n == lastN);

        // 验证 1:序号连续
        if (n != _expectedChunkNumber)
            throw new GridFSChunkException(_fileId, _expectedChunkNumber, "is missing");

        // 验证 2:非末块必须精确等于 chunkSizeBytes
        if (!isLastChunk && data.Length != _fileInfo.ChunkSizeBytes)
            throw new GridFSChunkException(_fileId, n, "is the wrong size");

        // 验证 3:末块不能超过 chunkSizeBytes
        if (isLastChunk && data.Length > _fileInfo.ChunkSizeBytes)
            throw new GridFSChunkException(_fileId, n, "is the wrong size");

        _expectedChunkNumber++;
        // 将 data 字节拷贝到调用方缓冲区...
    }
}

三重验证对应写入时的三条强制规则,形成完整的写入---读取一致性闭环。驱动不做 的两件事:不重新验证 files_id(查询已过滤);不默认重算 MD5(md5 字段已 deprecated)。

范围读取(Range Read)

GridFS 原生支持 HTTP Range 请求,视频播放器拖动进度条跳转的底层实现正是基于此:

javascript 复制代码
// 读取 500000 ~ 1000000 字节范围
const startChunk  = Math.floor(500000  / chunkSize);  // = 1
const endChunk    = Math.floor(1000000 / chunkSize);  // = 3
const startOffset = 500000  % chunkSize;              // 块内偏移

db.fs.chunks.find({
  files_id: fileId,
  n: { $gte: startChunk, $lte: endChunk }
}).sort({ n: 1 });
// 对首块截取 startOffset 之后的字节,对尾块按需截断

注意:驱动每次必须将完整的 chunk 加载进内存,不支持部分块加载。频繁随机跳转场景应选更小的 chunkSizeBytes


四、查看分块数量的方法

方法一:公式计算(首选,O(1))

fs.files 文档中已有 lengthchunkSize,直接计算,无需碰 fs.chunks

javascript 复制代码
const file = db.fs.files.findOne({ _id: fileId });
const totalChunks = Math.ceil(file.length / file.chunkSize);

方法二:实际计数(完整性验证,O(n))

javascript 复制代码
db.fs.chunks.countDocuments({ files_id: ObjectId("...") })

若计算值与实际计数不符,文件已损坏或上传中途中断。

方法三:聚合全库对比(生产巡检)

javascript 复制代码
db.fs.chunks.aggregate([
  { $group: { _id: "$files_id", actualChunks: { $sum: 1 } } },
  { $lookup: {
      from: "fs.files", localField: "_id",
      foreignField: "_id", as: "fileInfo"
  }},
  { $project: {
      actualChunks: 1,
      expectedChunks: {
        $ceil: { $divide: [
          { $arrayElemAt: ["$fileInfo.length",    0] },
          { $arrayElemAt: ["$fileInfo.chunkSize", 0] }
        ]}
      }
  }},
  // 只返回实际块数 ≠ 期望块数的异常文件
  { $match: { $expr: { $ne: ["$actualChunks", "$expectedChunks"] } } }
])

方法四:预存到 metadata(高频读取场景)

上传时一次计算写入,后续读取 O(1):

csharp 复制代码
new GridFSUploadOptions {
    Metadata = new BsonDocument {
        { "totalChunks", (long)Math.Ceiling((double)fileSize / chunkSize) },
        { "uploader",    userId  },
        { "processed",   false   }
    }
}
方法 时间复杂度 适用场景
公式计算 O(1) 日常单文件查询
countDocuments O(n),n 为该文件块数 完整性验证
聚合对比 O(N),N 为 chunks 总数 全库巡检
metadata 预存 O(1) 业务高频读取

五、ChunkSize 大小策略

三层配置体系(C# 驱动)

复制代码
规范默认值(255 KiB = 261120 字节)
    ↓ 可覆盖
GridFSBucketOptions.ChunkSizeBytes   ← Bucket 级,影响该 Bucket 所有上传
    ↓ 可覆盖(优先级最高)
GridFSUploadOptions.ChunkSizeBytes   ← 单次上传级,仅影响此文件
csharp 复制代码
// Bucket 级默认值
var bucket = new GridFSBucket(database, new GridFSBucketOptions {
    ChunkSizeBytes = 261120   // Nullable<int>,null = 使用规范默认
});

// 单次上传覆盖(仅此文件生效)
await bucket.UploadFromBytesAsync("video.mp4", bytes,
    new GridFSUploadOptions { ChunkSizeBytes = 1048576 });  // 1 MB

修改 Bucket 的 chunkSize 只影响新上传的文件 ,已有文件沿用写入时的值。读取时驱动始终从 fs.files 中读取该文件实际使用的 chunkSize,与 Bucket 当前配置无关。

为什么默认是 255 KiB 而不是 256 KiB

这是精心设计的工程决策,而非随意选择:

复制代码
一个 chunk 文档的 BSON 开销分析:

  _id:        12 字节(ObjectId)
  files_id:   12 字节(ObjectId)
  n:           4 字节(Int32)
  data 头:     5 字节(BinData 头部)
  字段名开销: ~30 字节("files_id"=9, "data"=5, "n"=2 等)
  文档结构:   ~15 字节(BSON 文档头、终止符等)
  ────────────────────────────────
  总 BSON 元数据开销 ≈ 78 字节

  255 × 1024 + 78 ≈ 261198 ≈ 256 KiB(贴近内存对齐边界)
  若选 256 × 1024,整个文档将略微超过 256 KiB

场景化选型策略| 场景 | 推荐值 | 核心理由 |

|---|---|---| | 流媒体大视频(4K) | 1 ~ 4 MB | 减少 chunks 文档总数,降低索引压力,顺序读取吞吐高 | | 常规文件(文档、图片) | 255 KiB(默认) | 平衡块数与 I/O,贴近 256 KiB 内存对齐,无需配置 | | 高频随机读(音频跳转) | 64 ~ 128 KB | Range Read 每次加载更少无效字节,延迟低 | | 海量小文件(≤ 16MB) | 不建议用 GridFS | 直接存 BinData 字段,避免双集合开销 | | 分片集群 | 4 MB | 减少跨分片文档,配合 files_id hashed 分片键 |

分片集群配置

在分片集群中,推荐以 files_id 的哈希值为分片键,确保同一文件所有块集中在少数分片:

javascript 复制代码
sh.shardCollection("mydb.fs.chunks", { files_id: "hashed" });
// 同一 files_id 的 chunks 哈希值集中
// → 重组时不需要跨分片聚合 → 读取性能大幅提升

六、一致性保障的边界

GridFS 通过写入顺序约定实现弱一致性,而非依赖事务,理解这个边界非常重要:

GridFS 保障的:

  • 读取方看到 fs.files 记录,意味着所有 chunks 已写完
  • 下载时三重验证能检测到序号缺失和大小异常
  • 唯一复合索引防止同一块被重复写入

GridFS 不保障的:

  • 上传中断不会自动回滚已写的 chunks(产生孤儿块)
  • 写入非多文档事务,无法原子地同时操作两个集合
  • 并发上传同名文件需要业务层控制

生产环境推荐使用 WriteConcern.Majority,降低副本集选举期间丢失 chunks 的概率:

csharp 复制代码
var bucket = new GridFSBucket(database, new GridFSBucketOptions {
    WriteConcern   = WriteConcern.Majority,
    ReadPreference = ReadPreference.Primary,
    ChunkSizeBytes = 261120
});

七、总结

GridFS 的设计是一套精心权衡的工程方案,五条核心结论:

① 写入原则chunks 先落盘,files 最后写。fs.files 记录是文件"可见"的唯一信号。

② 读取原则 :先查 fs.fileschunkSizelength,再按 {files_id, n} 复合索引顺序扫描 chunks,三重验证保障完整性。

③ 块数查询 :首选 ⌈ length / chunkSize ⌉ 公式计算,O(1);验证场景用 countDocuments;高频业务场景预存到 metadata

④ ChunkSize 选型 :随机访问多用小块(64~128 KB),顺序读取用大块(1~4 MB),通用场景用默认(255 KiB)。chunkSize每个文件独立 的属性,写入时确定,存在 fs.files 中,与 Bucket 当前配置无关。

⑤ 弱一致性:GridFS 不是事务系统,依赖写入顺序保障弱一致性,需要业务层处理孤儿块清理和并发控制。

相关推荐
青云计划1 小时前
Mysql
数据库·mysql
SelectDB1 小时前
Agent 应用范式下,企业数据基础设施如何演进?
大数据·数据库·数据分析
杜子不疼.1 小时前
【C++ AI 大模型接入 SDK】 - 环境搭建
开发语言·数据库·c++
qq_283720051 小时前
Milvus 向量数据库全链路优化实战教程
数据库·milvus
m0_702036531 小时前
CSS如何兼容新旧方案结合响应式容器查询
jvm·数据库·python
ClouGence2 小时前
我们做了个疯狂的决定,把 CloudDM 全部开源了
数据库·后端·mysql
努力努力再努力wz2 小时前
【Qt入门系列】深入理解信号与槽:从事件响应到自定义信号机制
c语言·开发语言·数据结构·数据库·c++·qt·mysql
ℳ₯㎕ddzོꦿ࿐2 小时前
实战指南:使用 Docker Compose 优雅部署 MongoDB 并自动初始化用户
mongodb·docker·容器
2501_921939262 小时前
Redis
数据库·redis·缓存