60、MongoDB GridFS:大文件存储、分块上传/下载、元数据管理、C语言实现

说实话,MongoDB 本身有个 16MB 的文档大小限制。你想想看,一个高清视频、一个数据库备份文件,动不动就几百 MB 甚至几个 GB,直接塞进文档里肯定不行。GridFS 就是专门解决这个问题的——它把大文件切成小块,每块作为一个独立文档存储。

我个人习惯把 GridFS 理解成「文件分片系统」。它不改变 MongoDB 的存储引擎,只是在逻辑上把文件拆成 chunks(数据块)和 files(文件元数据)两个集合。嗯,这里要注意:GridFS 不是 MongoDB 的「文件系统」,它只是一个约定好的存储模式。

GridFS 的核心原理

说白了就两步:

  • 分块存储:把大文件切成固定大小的块(默认 255KB),每个块存为一个文档
  • 元数据管理:记录文件名、大小、块数、MD5 等信息,存在另一个集合里

我在项目中遇到过需要存储 2GB 的日志压缩包,直接用 GridFS 搞定。当时最头疼的不是存储,而是如何高效地读取中间某一段数据——GridFS 的块索引机制帮了大忙。

GridFS 的集合结构

每个 GridFS 存储桶(bucket)默认包含两个集合:

集合名 用途 关键字段
fs.files 存储文件元数据 _id, filename, length, chunkSize, uploadDate, md5
fs.chunks 存储文件数据块 _id, files_id, n, data

你想想看,files_id 关联到 fs.files_idn 是块序号。这样读取时就能按顺序拼接,或者直接跳到指定块。

C 语言实现 GridFS 上传

我用的是 mongo-c-driver 库。先安装好 libmongoc 和 libbson,然后开始写代码。

下面是一个完整的文件上传示例:

#include <mongoc/mongoc.h>
#include <stdio.h>
#include <string.h>

int upload_file_to_gridfs(const char *uri, const char *db_name,
                          const char *local_file, const char *gridfs_name) {
    mongoc_client_t *client;
    mongoc_gridfs_t *gridfs;
    mongoc_gridfs_file_t *file;
    mongoc_gridfs_file_opt_t opts = {0};
    bson_error_t error;
    FILE *fp;
    char buf[65536];
    size_t bytes_read;

    // 初始化
    mongoc_init();
    client = mongoc_client_new(uri);
    if (!client) {
        fprintf(stderr, "无法创建客户端\n");
        return -1;
    }

    // 打开 GridFS 存储桶
    gridfs = mongoc_client_get_gridfs(client, db_name, "fs", &error);
    if (!gridfs) {
        fprintf(stderr, "获取 GridFS 失败: %s\n", error.message);
        mongoc_client_destroy(client);
        return -1;
    }

    // 打开本地文件
    fp = fopen(local_file, "rb");
    if (!fp) {
        perror("打开本地文件失败");
        mongoc_gridfs_destroy(gridfs);
        mongoc_client_destroy(client);
        return -1;
    }

    // 设置元数据
    opts.filename = gridfs_name;
    opts.content_type = "application/octet-stream";
    // 可以添加自定义元数据
    bson_t *metadata = BCON_NEW("source", BCON_UTF8("c_program"),
                                "version", BCON_INT32(1));
    opts.metadata = metadata;

    // 创建 GridFS 文件
    file = mongoc_gridfs_create_file(gridfs, &opts);
    if (!file) {
        fprintf(stderr, "创建 GridFS 文件失败\n");
        fclose(fp);
        bson_destroy(metadata);
        mongoc_gridfs_destroy(gridfs);
        mongoc_client_destroy(client);
        return -1;
    }

    // 分块写入数据
    while ((bytes_read = fread(buf, 1, sizeof(buf), fp)) > 0) {
        mongoc_gridfs_file_write(file, buf, bytes_read);
    }

    // 保存文件
    if (!mongoc_gridfs_file_save(file)) {
        fprintf(stderr, "保存 GridFS 文件失败\n");
    } else {
        printf("上传成功!文件 ID: ");
        bson_oid_t oid;
        mongoc_gridfs_file_get_id(file, &oid);
        char oid_str[25];
        bson_oid_to_string(&oid, oid_str);
        printf("%s\n", oid_str);
    }

    // 清理
    mongoc_gridfs_file_destroy(file);
    bson_destroy(metadata);
    fclose(fp);
    mongoc_gridfs_destroy(gridfs);
    mongoc_client_destroy(client);
    mongoc_cleanup();

    return 0;
}

int main() {
    return upload_file_to_gridfs(
        "mongodb://localhost:27017",
        "test_db",
        "/path/to/large_file.bin",
        "my_large_file.bin"
    );
}

关键点:

  • mongoc_gridfs_create_file 创建文件句柄,此时还没写入数据
  • mongoc_gridfs_file_write 每次写入一块,内部自动分块
  • mongoc_gridfs_file_save 最终提交,写入元数据到 fs.files

C 语言实现 GridFS 下载

下载其实就是反向操作:按文件名找到文件,然后逐块读取。

int download_file_from_gridfs(const char *uri, const char *db_name,
                              const char *gridfs_name, const char *output_file) {
    mongoc_client_t *client;
    mongoc_gridfs_t *gridfs;
    mongoc_gridfs_file_t *file;
    mongoc_gridfs_file_list_t *list;
    bson_error_t error;
    FILE *fp;
    char buf[65536];
    size_t bytes_read;

    mongoc_init();
    client = mongoc_client_new(uri);
    gridfs = mongoc_client_get_gridfs(client, db_name, "fs", &error);

    // 按文件名查找
    bson_t query = BCON_NEW("filename", BCON_UTF8(gridfs_name));
    list = mongoc_gridfs_find(gridfs, &query);
    bson_destroy(&query);

    file = mongoc_gridfs_file_list_next(list);
    if (!file) {
        fprintf(stderr, "未找到文件: %s\n", gridfs_name);
        mongoc_gridfs_file_list_destroy(list);
        mongoc_gridfs_destroy(gridfs);
        mongoc_client_destroy(client);
        return -1;
    }

    fp = fopen(output_file, "wb");
    if (!fp) {
        perror("打开输出文件失败");
        mongoc_gridfs_file_destroy(file);
        mongoc_gridfs_file_list_destroy(list);
        mongoc_gridfs_destroy(gridfs);
        mongoc_client_destroy(client);
        return -1;
    }

    // 逐块读取并写入本地文件
    while ((bytes_read = mongoc_gridfs_file_read(file, buf, sizeof(buf))) > 0) {
        fwrite(buf, 1, bytes_read, fp);
    }

    printf("下载完成!文件大小: %" PRId64 " 字节\n",
           mongoc_gridfs_file_get_length(file));

    fclose(fp);
    mongoc_gridfs_file_destroy(file);
    mongoc_gridfs_file_list_destroy(list);
    mongoc_gridfs_destroy(gridfs);
    mongoc_client_destroy(client);
    mongoc_cleanup();

    return 0;
}

我的经验: 下载大文件时,建议用 mongoc_gridfs_file_seek 实现断点续传。我曾经处理过一个 5GB 的数据库备份,网络中断后从头下载,浪费了半小时。后来加了 seek 逻辑,从断点处继续读 chunks,效率提升明显。

元数据管理实战

GridFS 的元数据不只是文件名和大小。你可以往 metadata 里塞任何 BSON 数据。比如:

// 查询所有上传时间在某个范围内的文件
bson_t query = BCON_NEW("uploadDate",
    BCON_DATE_TIME(1700000000000),  // 毫秒时间戳
    "$gte", BCON_DATE_TIME(1705000000000));

mongoc_gridfs_file_list_t *list = mongoc_gridfs_find(gridfs, &query);

// 遍历结果
mongoc_gridfs_file_t *f;
while ((f = mongoc_gridfs_file_list_next(list)) != NULL) {
    printf("文件名: %s, 大小: %" PRId64 "\n",
           mongoc_gridfs_file_get_filename(f),
           mongoc_gridfs_file_get_length(f));
    mongoc_gridfs_file_destroy(f);
}
mongoc_gridfs_file_list_destroy(list);

注意: uploadDate 字段是 GridFS 自动生成的,你无法手动修改。如果你需要自定义时间字段,请放在 metadata 里。

GridFS 核心流程

下面这张图展示了上传和下载的完整流程:

GridFS 上传/下载核心流程 上传流程 打开本地文件 创建 GridFS 文件 分块写入数据 保存文件 下载流程 按文件名查询 获取文件句柄 逐块读取数据 写入本地文件 fs.files 存储元数据(文件名、大小、块数等) fs.chunks 存储数据块(files_id + n + data)

避坑指南

我曾经踩过几个坑,分享给你:

  • 块大小选择:默认 255KB 适合大多数场景。如果文件特别大(比如 10GB+),建议增大到 1MB,减少 chunks 数量,提升查询效率。
  • MD5 校验:GridFS 自动计算 MD5,但只在上传完成后才写入。如果你需要实时校验,得自己算。
  • 索引优化fs.chunks 默认有 files_id + n 的联合索引。如果你经常按文件名查询,建议给 fs.files.filename 加索引。
  • 内存管理:大文件上传时,mongoc_gridfs_file_write 内部会缓存数据。记得及时调用 save,否则内存占用会飙升。

一句话总结: GridFS 就是 MongoDB 给大文件准备的「切菜板」。把大文件切成小块,存到 chunks 里,元数据放 files 里。C 语言用 mongo-c-driver 操作起来很直接,注意块大小和索引优化就行。


公众号:蓝海资料掘金营,微信deep3321