代码改变世界

GridFS

  abce  阅读(243)  评论(0编辑  收藏  举报

 

1.概述

如果文件大小超过 16 MB 的 BSON 文档大小限制,可以使用 GridFS来存储和检索。GridFS 不将文件存储在一个文档中,而是大型数据进行分块处理,然后将这些切分后的小文档保存在数据库中。

 

2.GridFS 的工作原理

GridFS 在存储桶中组织文件,存储桶是一组包含文件块和描述性信息的 MongoDB 集合。存储桶包含以下集合,使用 GridFS 规范中定义的约定命名:

  • chunks 集合存储二进制文件块。
  • files 集合存储文件元数据。

创建新的 GridFS 桶时,驱动程序会创建 chunksfiles 集合,并以默认桶名称 fs 作为前缀,除非用户显式指定其他名称。驱动程序还会在每个集合上创建索引,以确保有效检索文件和相关元数据。如果 GridFS 桶尚不存在,驱动程序仅在第一次写入操作时创建该桶。仅当索引不存在且桶为空时,驱动程序才会创建索引。

使用 GridFS 存储文件时,驱动程序会将文件拆分成小块,每个块由 chunks 集合中的单个文档表示。它还在 files 集合中创建一个文档,其中包含唯一的文件 ID、文件名和其他文件元数据。可以从内存或数据流上传文件。下图描述了 GridFS 在将文件上传到存储桶时如何分割文件:

在检索文件时,GridFS 从指定存储桶上的 files 集合中获取元数据,并使用该信息通过 chunks 集合中的文档重建文件。用户可以将文件读取到内存中,或者将其输出到流。

2.1GridFS集合

GridFS将文件存储到两个集合中:chunks和files。GridFS将集合放到一个桶中并以桶名字作为前缀,默认的桶名是fs。因此两个集合是:fs.files和fs.chunks。

支持自定义桶的名字,单个数据库中可以创建多个桶。

2.2集合fs.files

1
2
3
4
5
6
7
8
9
10
11
{
  "_id" : <ObjectId>,      #文档的标识符,具有唯一性
  "length" : <num>,        #文档的长度,单位是bytes
  "chunkSize" : <num>,     #块大小,单位是bytes
  "uploadDate" : <timestamp>,#上传的时间
  "md5" : <hash>,            #md校验和,已经被取消
  "filename" : <string>,     #人可读的文件文件名,可选项
  "contentType" : <string>,
  "aliases" : <string array>,
  "metadata" : <any>,
}

2.3集合fs.chunks

集合中的每个文档表示文件的一个块。文档格式如下:

1
2
3
4
5
6
{
  "_id" : <ObjectId>,    #块标识符,具有唯一性
  "files_id" : <ObjectId>,#文件的id,对应fs.files中的集合
  "n" : <num>,            #块的序列号
  "data" : <binary>       #bson格式的数据
}

2.4GridFS索引

GridFS 在每个块和文件集合上使用索引,以提高效率。为方便起见,符合 GridFS 规范的驱动程序会自动创建这些索引。你也可以根据应用程序的需要创建其他索引。

chunks 索引

GridFS 使用 files_id 和 n 字段对 chunks 集合建立唯一的组合索引。这样就能高效检索数据块,如下例所示:

1
db.fs.chunks.find( { files_id: myFileID } ).sort( { n: 1 } )

符合 GridFS 规范的驱动程序会在读写操作之前自动确保该索引存在。如果该索引不存在,可以执行以下操作,使用 mongosh 创建该索引:

1
db.fs.chunks.createIndex( { files_id: 1, n: 1 }, { unique: true } );

files索引

GridFS 使用文件名和上传日期字段为files集合创建索引。如本例所示,通过该索引可以高效地检索文件:

1
db.fs.files.find( { filename: myFileName } ).sort( { uploadDate: 1 } )

符合 GridFS 规范的驱动程序会在读写操作之前自动确保该索引存在。如果该索引不存在,可以执行以下操作,使用 mongosh 创建该索引:

1
db.fs.files.createIndex( { filename: 1, uploadDate: 1 } );

 

3.分片 GridFS

有两个集合需要考虑--files和chunks。

3.1chunks集合

要对 chunks 集合进行分片,可以使用 { files_id : 1, n : 1 } 或 { files_id : 1 } 作为分片键索引。

 

对于不运行 filemd5 来验证上传是否成功的 MongoDB 驱动程序,可以使用哈希分片来处理chunks集合。

 

如果 MongoDB 驱动程序运行 filemd5,则不能使用哈希分片。

3.2files集合

文件集很小,只包含元数据。GridFS 所需的键都不适合在分片环境中均匀分布。如果不对files集合进行分片,所有文件元数据文件都可以保存在主分片上。

 

如果必须对files集合进行分片,可以使用 _id 字段,也可以与应用程序字段结合使用。

 

4.何时使用 GridFS

在 MongoDB 中,使用 GridFS 来存储大于 16 MB 的文件。

 

某些情况下,在 MongoDB 数据库中存储大文件可能比在系统级文件系统中存储更有效:

·如果文件系统限制目录中的文件数量,可以使用 GridFS 来存储所需的文件。

·如果希望访问大文件中的部分信息,而无需将整个文件加载到内存中,则可以使用 GridFS 来调用文件的部分内容,而无需将整个文件读入内存。

·当想保持文件和元数据自动同步,并在多个系统和设施间部署时,可以使用 GridFS。在使用地理分布复制集时,MongoDB 可以将文件及其元数据自动分发到多个 mongod 实例和设施。

 

如果需要原子式更新整个文件的内容,请不要使用 GridFS。作为替代方案,你可以为每个文件存储多个版本,并在元数据中指定文件的当前版本。上传新版本的文件后,可以在原子更新中更新表示 “最新 ”状态的元数据字段,之后再根据需要删除以前的版本。

 

此外,如果文件都小于 16 MB BSON 文档大小限制,可以考虑将每个文件存储在单个文档中,而不是使用 GridFS。可以使用 BinData 数据类型来存储二进制数据。

 

5.使用GridFS

要使用GridFS,有两种方式:

·使用mongodb驱动器

·使用mongofiles命令行工具

5.1创建 GridFS 存储桶

创建一个存储桶或获取对现有存储桶的引用,以开始从 GridFS 存储或检索文件。创建一个 GridFSBucket 实例,并将数据库作为参数传递。然后,就可以使用 GridFSBucket 实例对存储桶中的文件调用读取和写入操作:

1
2
const db = client.db(dbName);
const bucket = new mongodb.GridFSBucket(db);

将存储桶名称作为第二个参数传递给 create() 方法,以创建或引用具有默认名称 fs 之外的自定义名称的存储桶,如以下示例所示:

1
const bucket = new mongodb.GridFSBucket(db, { bucketName: 'myCustomBucket' });

5.2上传文件

使用 GridFSBucket 中的 openUploadStream() 方法为指定文件名创建上传流。可以使用 pipe() 方法将 Node.js 读取流连接到上传流。使用 openUploadStream() 方法可以指定配置信息,如文件数据块大小和其他作为元数据存储的字段/值对。

以下示例展示了如何将由变量 fs 表示的 Node.js 读取流通过管道传递给 GridFSBucket 实例的 openUploadStream() 方法:

1
2
3
4
5
fs.createReadStream('./myFile').
     pipe(bucket.openUploadStream('myFile', {
         chunkSizeBytes: 1048576,
         metadata: { field: 'myField', value: 'myValue' }
     }));

5.3检索文件信息

元数据包含所引用文件的相关信息,包括:

  • 文件的 _id
  • 文件的名称
  • 文件的长度/大小
  • 上传日期和时间
  • 可以在其中存储任何其他信息的 metadata 文档

GridFSBucket 实例上调用 find() 方法,以便从 GridFS 存储桶中检索文件。该方法会返回一个 FindCursor 实例,可以从该实列访问结果。

以下代码示例向你展示如何从 GridFS 存储桶中的所有文件中检索和打印文件元数据。在从 FindCursor 可迭代表中遍历检索结果的各种方法中,以下示例使用 for await...of 事务语法来显示结果:

1
2
3
4
const cursor = bucket.find({});
for await (const doc of cursor) {
   console.log(doc);
}

find() 方法可接受各种查询说明,可与 sort()limit()project() 等其他方法结合使用。

5.4下载文件

可以使用 GridFSBucket 中的 openDownloadStreamByName() 方法创建下载流,从 MongoDB 数据库下载文件。

下面的示例展示了如何将存储在 filename 字段中的文件名引用的文件下载到工作目录中:

1
2
bucket.openDownloadStreamByName('myFile').
     pipe(fs.createWriteStream('./outputFile'));

注意:

如果存在多个具有相同 filename 值的文档,GridFS 将流式传输具有给定名称(由 uploadDate 字段决定)的最新文件。

或者可以使用 openDownloadStream() 方法将文件的 _id 字段作为参数:

1
2
bucket.openDownloadStream(ObjectId("60edece5e06275bf0463aaf3")).
     pipe(fs.createWriteStream('./outputFile'));

注意:

GridFS 流 API 无法加载块的部分数据。当下载流需要从 MongoDB 拉取一个数据块,它会将整个数据块拉入内存。255 KB 默认数据段大小通常足够。

5.5重命名文件

使用 rename() 方法更新存储桶中 GridFS 文件的名称。必须用文件的 _id 字段而不是文件名来指定要重命名的文件。

注意:

rename() 方法每次仅支持更新一个文件的名称。要重命名多个文件,请从存储桶中检索与文件名匹配的文件列表,从要重命名的文件中提取 _id 字段(Field),然后将每个值分别传递到 rename() 方法。

以下示例显示如何通过引用文档的 _id 字段(Field)将 filename 字段(Field)更新为“newFileName”:

1
bucket.rename(ObjectId("60edece5e06275bf0463aaf3"), "newFileName");

5.6删除文件

使用 delete() 方法以从存储桶中删除文件。必须用文件的 _id 字段而不是文件名来指定该文件。

注意:

delete() 方法一次只支持删除一个文件。要删除多个文件,请从存储桶中检索文件,提取要删除的文件中的 _id 字段,将每个值分别传递给 delete() 方法。

下面的示例展示了如何通过引用文件的 _id 字段来删除文件:

1
bucket.delete(ObjectId("60edece5e06275bf0463aaf3"));

5.7删除 GridFS 存储桶

使用 drop() 方法删除存储桶的 fileschunks 集合,从而有效删除存储桶。以下代码示例展示了如何删除 GridFS 存储桶:

1
bucket.drop();

 

6.GridFS 的局限性

事实上,没有放之四海而皆准的解决方案,MongoDB GridFS 也不例外。因此,请记住这些限制:

·从数据库中以多个块的形式持续提供大文件确实会影响工作集(一个 16MB 的文件会被检索为 65 个分块,每个分块 255KB),尤其是在处理千兆字节或万兆字节数据的情况下。

(针对这一点可以考虑使用专门的实例来提供GridFS功能、并增加块的默认大小)

·从数据库提供文件比从文件系统提供文件要慢一些。

·GridFS 本身并不提供原子更新整个文件的方法。因此,如果系统经常更新整个文件,就不要使用 GridFS 或使用下面讨论的变通方法。

(针对这一点可以考虑引入版本控制的思想,不过效果可能并不理想)

 

相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2022-09-22 【SQLServer】SQLServer执行计划概览
2022-09-22 【SQLServer 】SQLServer健康报告
2015-09-22 RMAN Recovery Catalog
点击右上角即可分享
微信分享提示