本文最后更新于:2024年8月16日 下午

GridFS 是MongoDB 的一个子模块,使用 GridFS 可以基于 MongoDB 来持久存储文件,并且支持分布式应用(文件分布存储和读取)。本文介绍相关内容。

背景

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 json 的 bson 格式,因此可以存储比较复杂的数据类型。

MongoDB 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

MongoDB 提供了高性能、高可用、支持分片及面向文档等特性,是 Nodejs 应用程序最受欢迎的非关系型数据之一。

但是 MongoDB 以 BSON 格式存储数据,其最多只能存储16MB 的数据。这样做的原因是为了避免单个文档占用太多 RAM 或在事务期间过度使用带宽。

这就是 GridFS 可以大显身手的地方。为了存储大于16MB 的数据,GridFSAPI 将数据划分为更小的大小,称为块。在检索时,可以组合数据块以获得相同的数据。每个块都是该数据文件部分的二进制表示形式。

简介

GridFS 是MongoDB 的一个子模块,使用 GridFS 可以基于 MongoDB 来持久存储文件,并且支持分布式应用(文件分布存储和读取)。

GridFS 是 MongoDB 内置的功能,用于存储和检索大于 BSON 文档大小限制的文件,如图片和视频。

GridFS是MongoDB提供的二进制数据存储在数据库中的解决方案,对于 MongoDB 的BSON 格式的数据(文档)存储有尺寸限制,最大为 16M。但是在实际系统开发中,上传的图片或者文件可能尺寸会很大,此时我们可以借用GridFS 来管理这些文件。

  • GridFS 常用的使用场景

如果你的文件系统在一个目录中存储的文件的数量有限,你可以使用 GridFS存储尽可能多的文件。

当你想访问大型文件的部分信息,却不想加载整个文件到内存时,您可以使用GridFS 存储文件,并读取文件部分信息,而不需要加载整个文件到内存。

当你想让你的文件和元数据自动同步并部署在多个系统和设施,你可以使用GridFS 实现分布式文件存储。

存储原理

GridFS 使用两个集合(collection)存储文件。一个集合是 chunks, 用于存储文件内容的二进制数据;一个集合是 files,用于存储文件的元数据。

GridFS 会将两个集合放在一个普通的 buket 中,并且这两个集合使用 buket 的名字作为前缀。MongoDB 的 GridFs 默认使用 fs 命名的 buket 存放两个文件集合。因此存储文件的两个集合分别会命名为集合 fs.files ,集合 fs.chunks。

当然也可以定义不同的 buket 名字,甚至在一个数据库中定义多个 bukets,但所有的集合的名字都不得超过 MongoDB 命名空间的限制。

MongoDB 集合的命名包括了数据库名字与集合名字,会将数据库名与集合名通过“.”分隔,而且命名的最大长度不得超过 120bytes。

当把一个文件存储到 GridFS 时,如果文件大于 chunksize (每个 chunk 块大小为 256KB),会先将文件按照 chunk 的大小分割成多个 chunk 块,最终将 chunk 块的信息存储在 fs.chunks 集合的多个文档中。然后将文件信息存储在 fs.files 集合的唯一一份文档中。其中 fs.chunks 集合中多个文档中的 file_id 字段对应 fs.files 集中文档 _id 字段。

读文件时,先根据查询条件在 files 集合中找到对应的文档,同时得到 _id 字段,再根据 _id 在chunks 集合中查询所有 “files_id” 等于_id 的文档。最后根据“n”字段顺序读取 chunk 的“data”字段数据,还原文件。

fs.files 集合存储文件的元数据,以类 json 格式文档形式存储。每在GridFS 存储一个文件,则会在 fs.files 集合中对应生成一个文档。

fs.files 集合中文档的存储内容如下:

1
2
3
4
5
6
7
8
9
10
{
"_id": <ObjectId>, // 文档 ID,唯一标识
"chunkSize": <num>, // chunk 大小 256kb
"uploadDate": <timetamp>, //文件上传时间
"length": <num>, // 文件长度
"md5": <string>, // 文件 md5 值 (7.0 已弃用)
"filename": <string>, // 文件名
"contentType": <string>,// 文件的?MIME类型 (7.0 已弃用)
"metadata": <dataObject>// 文件自定义信息
}

fs.chunks 集合存储文件文件内容的二进制数据,以类 json 格式文档形式存储。每在 GridFS 存储一个文件,GridFS 就会将文件内容按照 chunksize 大小(chunk 容量为 256k)分成多个文件块,然后将文件块按照类 json 格式存在.chunks 集合中,每个文件块对应 fs.chunk 集合中一个文档。一个存储文件会对应一到多个 chunk 文档。

fs.chunks 集合中文档的存储内容如下:

1
2
3
4
5
6
{
"_id": <ObjectId>, // 文档 ID,唯一标识
"files_id": <ObjectId>, // 对应 fs.files 文档的 ID
"n": <num>, // 序号,标识文件的第几个 chunk
"data": <binary> // 文件二级制数据
}

为了提高检索速度 MongoDB 为 GridFS 的两个集合建立了索引。fs.files 集合使用是“filename”与“uploadDate” 字段作为唯一、复合索引。fs.chunk 集合使用的是“files_id”与“n”字段作为唯一、复合索引。

GridFS 使用

shell 命令之 mongofiles

MongoDB 提供 mongofiles 工具,可以使用命令行来操作 GridFS。主要有四个命令:

Put

1
2
3
#mongofiles -h -u  -p  --db files put /conn.log
connected to: 127.0.0.1
added file: { _id: ObjectId('530cf1009710ca8fd47d7d5d'),filename: "./conn.log", chunkSize: 262144, uploadDate: newDate(1393357057021), md5: "6515e95f8bb161f6435b130a0e587ccd", length:1644981 }

Get

1
2
3
#mongofiles -h -u  -p  --db files get /conn.log
connected to: 127.0.0.1
done write to: ./conn.log

List

1
2
3
# mongofiles -h -u  -p  list
connected to: 127.0.0.1
/conn.log 1644981

Delete

1
2
[root@ip-10-198-25-43 tmp]# mongofiles -h  -u -p  --db files delete /conn.log
connected to: 127.0.0.1

MongoDB API

MongoDB 支持多种编程语言驱动,比如 c、java、python、C#、nodeJs 等。因此可以使用这些语言 MongoDB 驱动 API 操作,扩展 GridFS。

磁盘空间优化

MongoDB 不会释放已经占用的硬盘空间。即使删除 db 中的集合 ,MongoDB 也不会释放磁盘空间。同样,如果使用 GridFS 存储文件,从 GridFS 存储中删除无用的垃圾文件,MongoDB 依然不会释放磁盘空间的。这会造成磁盘一直在消耗,而无法回收利用的问题。

那怎样才能释放磁盘空间呢?

1.可以通过修复数据库来回收磁盘空间,即在 mongo shell 中运行 db.repairDatabase()命令或者 db.runCommand({repairDatabase: 1 }) 命令(此命令执行比较慢)。

使用通过修复数据库方法回收磁盘时需要注意,待修复磁盘的剩余空间必须大于等于存储数据集占用空间加上 2G,否则无法完成修复。因此使用 GridFS 大量存储文件必须提前考虑设计磁盘回收方案,以解决MongoDB 磁盘回收问题。

2.使用 dump & restore 方式,即先删除 MongoDB 数据库中需要清除的数据,然后使用 mongodump 备份数据库。备份完成后,删除 MongoDB 的数据库,使用 Mongorestore 工具恢复备份数据到数据库。

当使用 db.repairDatabase()命令没有足够的磁盘剩余空间时,可以采用 dump & restore 方式回收磁盘资源。如果 MongoDB 是副本集模式,dump & restore 方式可以做到对外持续服务,在不影响 MongoDB正常使用下回收磁盘资源。

MongoDB 使用副本集, 实践使用 dump & restore 方式,回收磁盘资源。70G 的数据在 2 小时之内完成数据清理及磁盘回收,并且整个过程不影响 MongoDB 对外服务,同时可以保证处理过程中数据库增量数据的完整。

注意

GridFs 并非银弹,它还是有一些局限性:

  1. 存储规模,如果你的存储量是不断增加的,或者你预估的规模是比较大的话,还是建议存储到文件服务器上。
  2. 原子更新,GridFs 没有提供对文件的原子更新方式。

Python 调用

需要安装 pymongo 库

1
pip install pymongo

创建 GridFS 对象

1
2
3
4
5
6
7
8
9
10
from pymongo import MongoClient
from gridfs import GridFS


if __name__ == '__main__':
#建立MongoDB数据库连接
conn = MongoClient(host='127.0.0.1',port=27017)
mydb = conn['test']

fs = GridFS(mydb)

保存数据

在创建 GridFS 对象后可以用 fs 对象插入数据

1
2
3
4
5
6
7
8
9
10
image_path = 'test.jpg'
with open(image_path, 'rb') as file:
custom_id = 'my_img_id'
# 检查自定义ID是否已经存在
if not fs.exists(_id=custom_id):
fs.put(file.read(), _id=custom_id, filename=image_file_name, content_type='image/jpeg')
print(f"Image '{image_file_name}' saved with custom id: {custom_id}")
else:
print(f"Image '{image_file_name}' with custom id: {custom_id} already exists.")

读取数据

我们将刚刚保存的数据从数据库中读取出来并显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from PIL import Image
from io import BytesIO
from pymongo import MongoClient
from gridfs import GridFS

# MongoDB连接设置
mongo_host = 'your_mongodb_host'
mongo_port = 27017 # 根据实际情况修改端口
mongo_db_name = 'image_database'
mongo_user = 'your_username'
mongo_password = 'your_password'

# 连接到MongoDB
client = MongoClient(mongo_host, mongo_port, username=mongo_user, password=mongo_password)
db = client[mongo_db_name]
fs = GridFS(db)

# 自定义ID,即你之前保存图像时使用的ID
custom_id = 'your_custom_image_id_here'

# 从GridFS读取图像
if fs.exists(_id=custom_id):
file = fs.get(custom_id)
image = Image.open(BytesIO(file.read()))
image.show()
print(f"Image with custom id: {custom_id} displayed.")
else:
print(f"No image found with custom id: {custom_id}")

# 关闭数据库连接
client.close()

列出数据

要列出GridFS中所有图像的ID,可以查询fs.files集合,这个集合存储了GridFS中文件的元数据,包括文件的ID。以下是如何使用Python和pymongo库来执行此操作的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pymongo import MongoClient
from gridfs import GridFS

# MongoDB连接设置
mongo_host = 'your_mongodb_host'
mongo_port = 27017 # 根据实际情况修改端口
mongo_db_name = 'image_database'
mongo_user = 'your_username'
mongo_password = 'your_password'

# 连接到MongoDB
client = MongoClient(mongo_host, mongo_port, username=mongo_user, password=mongo_password)
db = client[mongo_db_name]
fs = GridFS(db)

# 列出GridFS中所有图像的ID
try:
files = fs.find()
for file in files:
print(f"File ID: {file._id}")
except Exception as e:
print(f"An error occurred: {e}")

# 关闭数据库连接
client.close()

在这段代码中,fs.find()方法会返回一个迭代器,包含了fs.files集合中的所有文档。每个文档都是一个GridFS文件记录,包含文件的元数据,其中_id字段是文件的唯一标识符。

条件查询

可以添加一个查询过滤条件

1
2
3
4
5
# 列出GridFS中所有JPEG图像的ID
files = fs.find({'contentType': 'image/jpeg'})
for file in files:
print(f"JPEG Image ID: {file._id}")

这里,{'contentType': 'image/jpeg'}是一个查询过滤条件,它将结果限制为内容类型为JPEG图像的文件。

也可以设置其他过滤条件

基于上传日期的查询
1
2
3
4
5
6
7
8
from datetime import datetime

# 查询上传日期在特定日期之后的文件
upload_date = datetime(2023, 1, 1)
files = fs.find({'uploadDate': {'$gte': upload_date}})

for file in files:
print(f"File ID: {file._id}, Filename: {file.filename}, Upload Date: {file.uploadDate}")
基于文件大小的查询
1
2
3
4
5
6
# 查询文件大小大于特定值的文件
min_file_size = 1024 * 1024 * 5 # 5MB
files = fs.find({'length': {'$gte': min_file_size}})

for file in files:
print(f"File ID: {file._id}, Filename: {file.filename}, Size: {file.length} bytes")

删除文件

1
fs.delete(file_id)

要删除单个文件,你需要知道该文件的ID。以下是删除指定ID文件的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pymongo import MongoClient
from gridfs import GridFS

# MongoDB连接设置
mongo_host = 'your_mongodb_host'
mongo_port = 27017 # 根据实际情况修改端口
mongo_db_name = 'image_database'
mongo_user = 'your_username'
mongo_password = 'your_password'

# 连接到MongoDB
client = MongoClient(mongo_host, mongo_port, username=mongo_user, password=mongo_password)
db = client[mongo_db_name]
fs = GridFS(db)

# 要删除的文件的ID
file_id_to_delete = 'your_file_id_here'

# 删除文件
if fs.exists(file_id_to_delete):
fs.delete(file_id_to_delete)
print(f"File with ID {file_id_to_delete} has been deleted.")
else:
print(f"No file found with ID {file_id_to_delete}.")

# 关闭数据库连接
client.close()

如果你想根据某些条件删除多个文件,可以使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 删除条件,例如删除所有内容类型为'image/jpeg'的文件
delete_condition = {'contentType': 'image/jpeg'}

# 查询符合条件的文件
files_to_delete = fs.find(delete_condition)

# 删除每个找到的文件
for file in files_to_delete:
fs.delete(file._id)
print(f"File with ID {file._id} has been deleted.")

# 关闭数据库连接
client.close()

这段代码会删除所有内容类型为image/jpeg的文件。

注意事项

  • 删除操作是不可逆的,一旦执行,文件和相关的块数据将被永久删除。
  • 在执行删除操作之前,请确保你有适当的权限,并且你确实想要删除这些数据。
  • 如果你在删除过程中遇到问题,可能需要检查MongoDB服务器的日志来获取更多信息。

参考资料



文章链接:
https://www.zywvvd.com/notes/coding/dataset/mongodb-gridfs/mongodb-gridfs/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

MongoDB GridFS
https://www.zywvvd.com/notes/coding/dataset/mongodb-gridfs/mongodb-gridfs/
作者
Yiwei Zhang
发布于
2024年8月13日
许可协议