# Mongo 存储文件—GridFS

# 简介

  • GridFS 是用于存储和检索超过 BSON -文档大小限制为 16 MB 的文件的规范。

    • 文件都小于 16 MB 限制,可以将文件存入单个文档中,不使用 GridFS 进行存储
  • 在 MongoDB4.0 版本之前只支持单文档事务操作,在 4.0 版本之后开始支持多文档事务操作,但是 GridFS 不支持多文档事务。

    • 执行要么全部成功,要么全部失败来保证数据完整性。
    • 如果需要以原子方式更新整个文件的内容,作为替代方案,您可以存储每个文件的多个版本,并在元数据中指定文件的当前版本。您可以在上载新版本的文件后更新在原子更新中指示“最新”状态的元数据字段,并在以后删除以前版本(如果需要)。
  • GridFS 不是将文件存储在单个文档中,而是将文件分成多个部分或块[1],并将每个块存储为单独的文档。

    • 当您查询 GridFS 文件时,驱动程序将根据需要重新组装块。您可以对通过 GridFS 存储的文件执行范围查询。您还可以从文件的任意部分访问信息,例如“跳过”到视频或音频文件的中间。
    • 从大型文件的各个部分访问信息而无需将整个文件加载到内存中,可以使用 GridFS 调用文件的各个部分,而无需将整个文件读入内存。
  • 默认情况下,GridFS 使用默认的块大小 255 kB; 也就是说,GridFS 将文件分成 255 kB 的块,但最后一个块除外。最后一个块只有必要的大小。类似地,不大于块大小的文件只有最终块。

  • GridFS 使用两个集合来存储文件。一个集合存储文件块,另一个存储文件元数据。

# 什么时候使用 GridFS

  • 在 MongoDB 中,使用 GridFS 存储大于 16 MB 的文件。
  • 在某些情况下,在 MongoDB 数据库中存储大文件可能比在系统级文件系统上更高效。
    • 如果文件系统限制目录中的文件数,则可以使用 GridFS 根据需要存储任意数量的文件。
    • 大文件访问部分信息时更加高效
    • 如果要保持文件和元数据在多个系统和设施中自动同步和部署,可以使用 GridFS。MongoDB 可以自动将文件及其元数据分发到多个 mongod实例和工具中。

不支持原子方式更新整个文件的内容

# GridFS 怎样存文件

# 文件结构

mongoFile

  • GridFS 将文件存储在两个集合中
    • chunks存储二进制块
    • files存储文件的元数据
  • GridFS 通过在每个集合前面加上桶名(bucket name),将集合放在一个公共桶(common bucket)中。默认情况下,GridFS 使用两个集合和一个名为 fs 的桶
    • fs.files 对应生成一个文档。
    • fs.chunk 对应生成多个文档。
  • 读文件时,先根据查询条件在 files 集合中找到对应的文档,同时得到“_id”字段,再根据“_id”在 chunks 集合中查询所有“files_id”等于“_id”的文档。最后根据“n”字段顺序读取 chunk 的“data”字段数据,还原文件。

# chunks Collection

chunks 集合中的每个文档代表 GridFS 中表示的文件的不同块。此集合中的文档具有以下形式:

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

# files Collection

files集合中的每个文档都代表 GridFS 中的一个文件 。

{
  "_id": <ObjectId>,    // 文档ID,唯一标识
  "chunkSize": <num>,   // chunk大小 256kb
  "uploadDate": <timetamp>, //文件上传时间
  "length": <num>,      // 文件长度
  "md5": <string>,      // 文件md5值
  "filename": <string>, // 文件名
  "contentType": <string>,// 文件的MIME类型
  "metadata": <dataObject>// 文件自定义信息
}
1
2
3
4
5
6
7
8
9
10
  • GridFS 使用每个chunksfiles集合上的索引来提高效率。符合 GridFS 规范的驱动程序会自动创建这些索引以方便使用。
    • fs.files 集合使用是“filename”与“uploadDate” 字段作为唯一、复合索引。
    • fs.chunk 集合使用的是“files_id”与“n”字段作为唯一、复合索引。

# 如何使用 GridFS

# mongo files 命令行工具

  • mongofiles实用程序可以从命令行操作 GridFS 对象中存储在 MongoDB 实例中的文件
  • 从系统命令行运行,而不是 mongoshell 中

# 结构

mongofiles <options> <commands> <filename>
1

# 所需条件

  • 要使用该选项连接到mongod强制授权--auth您必须使用 --username--password选项。连接用户必须至少拥有:

    • read使用时,为访问数据库角色 listsearchget命令,
    • readWrite使用putdelete命令时访问的数据库的角色。
     mongofiles -u "writeUser" -p "passwd" --authenticationDatabase admin
    
    1

# 常用 commands

  • list <prefix>

    列出 GridFS 存储中的文件。list(例如<prefix>)之后指定的字符 可选地将返回项的列表限制为以该字符串开头的文件。

  • search <string>

    列出 GridFS 存储中的文件,其名称与任何部分匹配<string>

  • put <filename>

    将指定文件从本地文件系统复制到 GridFS 存储中。这里,<filename>指的是对象在 GridFS 中的名称,并mongofiles假设它反映了文件在本地文件系统上的名称。如果本地文件名不同,请使用该选项。mongofiles --local

  • get <filename>

    将指定文件从 GridFS 存储复制到本地文件系统。这里,<filename>指的是对象在 GridFS 中的名称。mongofiles使用filenameGridFS 中的文件将文件写入本地文件系统。要在本地文件系统上为文件选择其他位置,请使用该 --local选项。

  • get_id "<_id>"

    *版本 3.2.0 中的新功能。*将其指定的文件<_id>从 GridFS 存储复制到本地文件系统。这里<_id>指的是_idGridFS 中对象的扩展 JSON :从 MongoDB 4.2 开始,get_id可以接受 ObjectId 值或非 ObjectId 值<_id>。在 MongoDB 4.0 及更早版本中,get_id只接受<ObjectId>值。mongofiles使用filenameGridFS 中的文件将文件写入本地文件系统。要在本地文件系统上为文件选择其他位置,请使用该 --local选项。

  • delete <filename>

    从 GridFS 存储中删除指定的文件。

  • delete_id "<_id>"

    版本 3.2.0 中的新功能。<_id>从 GridFS 存储中删除由其指定的文件:从 MongoDB 4.2 开始,delete_id可以接受 ObjectId 值或非 ObjectId 值<_id>。在 MongoDB 4.0 及更早版本中,delete_id只接受<ObjectId>值。

# 常用 options

  • --host --port

    • 默认值为 localhost:27017
  • --username , -u --password--authenticationDatabase`

    • 指定用于向使用身份验证的 MongoDB 数据库进行身份验证的用户名。
  • --db`` <database>``, ``-d`` <database>

  • --uri

    --uri "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]"
    
    1

    以下命令行选项不能与--uri选项一起使用:

    • --host
    • --port
    • --db
    • --username
    • --password (如果 URI 连接字符串也包含密码)
    • --authenticationDatabase
    • --authenticationMechanism

    而是将这些选项指定为--uri 连接字符串的一部分。

  • --local<filename>, -l

    指定 get 和 put 操作的文件的本地文件系统名称。

    mongofiles putmongofiles 获取命令中,必需的<filename>修饰符指的是对象在 GridFS 中的名称。mongofiles假设这反映了本地文件系统上的文件名。此设置将覆盖此默认值。

# 举例

在系统的 shell 中运行命令

  • 创建一个可读写的用户 writeUser

  • 上传文件 js-yaml.js 到数据库 files 中

    mongofiles -u "writeUser" -p "passwd" --authenticationDatabase admin -d files put js-yaml.js
    
    1

    返回信息

    2019-08-23T14:09:05.642+0000	connected to: mongodb://localhost/
    2019-08-23T14:09:05.684+0000	added gridFile: js-yaml.js
    
    1
    2

第二次上传相同文件到数据库中,GridFS 不会对其进行覆盖,会存储两个相同文件

  • 查看数据库 files 中的文件列表

    mongofiles -u "writeUser" -p "passwd" --authenticationDatabase admin -d files list
    
    1

    返回信息

    2019-08-23T14:17:46.973+0000	connected to: mongodb://localhost/
    js-yaml.js	108442
    
    1
    2

# MongoDB 驱动程序(Java API)

  • API 可以参考官方文档,源码相关

# 项目实例

  • SpringBoot 导入 mongo,web-start,io 模块(2.1.7.RELEASE)

    <dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-mongodb</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
            <dependency>
                <groupId>commons-io</groupId>
                <artifactId>commons-io</artifactId>
                <version>2.0.1</version>
            </dependency>
        </dependencies>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
  • application.properties 中

    spring.data.mongodb.uri=mongodb://192.168.191.156:27017/files
    
    1
  • 在 springboot 启动类同级目录或者子目录下创建 GridFSApi.java

    package com.yufh.springbootmongofiles;
    
    import com.mongodb.client.gridfs.model.GridFSFile;
    import org.bson.types.ObjectId;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.mongodb.core.query.Criteria;
    import org.springframework.data.mongodb.core.query.Query;
    import org.springframework.data.mongodb.gridfs.GridFsResource;
    import org.springframework.data.mongodb.gridfs.GridFsTemplate;
    import org.springframework.stereotype.Controller;
    import java.io.*;
    
    
    /**
     * @author yufh
     * @describe
     * @date 2019/8/23
     */
    @Controller
    public class GridFSApi {
    
        @Autowired
        private GridFsTemplate gridFsTemplate;
    
        //上传文件
        public void upload() {
            File file = new File("D:\\1.jpg");
            try {
                ObjectId id = gridFsTemplate.store(new FileInputStream(file), file.getName());
                System.out.println(id);
    
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        //下载文件
        public void download( String fileName) throws IOException {
            Query query = Query.query(Criteria.where("filename").is(fileName));
            GridFSFile  mongoFile = gridFsTemplate.findOne(query);
            //gridfs下载在2.x以下版本为
    
    //        try {
    //            mongoFile.writeTo("D:\\a.txt");
    //        } catch (IOException e) {
    //            e.printStackTrace();
    //        }
    
            //2.x以上版本。find的返回值进行了更改。
            GridFsResource gs = gridFsTemplate.getResource(mongoFile);
            OutputStream os = null;
            try {
                byte[] bs = new byte[1024];
                int len;
                InputStream in = gs.getInputStream();
                File file = new File("D:\\");
                os = new FileOutputStream(file.getPath() + File.separator + gs.getFilename());
                while ((len = in.read(bs)) != -1) {
                    os.write(bs, 0, len);
                }
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                os.close();
            }
    
        }
    
    	//删除文件
        public void delete( String fileName) {
            Query query = Query.query(Criteria.where("filename").is(fileName));
            gridFsTemplate.delete(query);
        }
    }
    
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
  • test 测试类

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringbootMongofilesApplicationTests {
    	@Resource GridFSApi gridFSApi;
    
    	@Test
    	public void downloadFiles() throws IOException {
    		gridFSApi.download("a.txt");
    	}
    	@Test
    	public void uploadFiles() {
    		gridFSApi.upload();
    	}
    
    	@Test
    	public void deleteFiles() {
    		gridFSApi.delete("1.jpg");
    	}
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

# 注意事项

  • GridFs 不会自动处理 md5 值相同的文件,也就是说,同一个文件进行两次 put 命令,将会在 GridFS 中对应两个不同的存储,对于存储来说,这是一种浪费。对于 md5 相同的文件,如果想要在 GridFS 中只有一个存储,需要通过 API 进行扩展处理。

  • 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 正常使用下回收磁盘资源。

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

参考链接:http://rdc.hundsun.com/portal/article/703.html