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 的 _id,n 是块序号。这样读取时就能按顺序拼接,或者直接跳到指定块。
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 核心流程
下面这张图展示了上传和下载的完整流程:
避坑指南
我曾经踩过几个坑,分享给你:
- 块大小选择:默认 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