Spring Boot使用mongo的GridFS模块
1. GridFS 简介
GridFS 是 Mongo 的一个子模块, 使用 GridFS 可以基于 MongoDB 来持久存储文件。并且支持分布式应用 (文件分布存储和读取)。作为 MongoDB 中二进制数据存储在数据库中的解决方案,通常用来处理大文件,对于 MongoDB 的 BSON 格式的数据(文档) 存储有尺寸限制,最大为 16M。但是在实际系统开发中,上传的图片或者文件可能尺寸会很大,此时我们可以借用 GridFS 来辅助管理这些文件。
GridFS 不是 MongoDB 自身特性,只是一种将大型文件存储在 MongoDB 的文件规范,所有官方支持的驱动均实现了 GridFS 规范。GridFS 制定大文件在数据库中如何处理,通过开发语言驱动来完成、通过 API 接口来存储检索大文件。
2. GridFS 使用场景
(1) 如果您的文件系统在一个目录中存储的文件的数量有限,你可以使用 GridFS 存储尽可能多的文件。
(2) 当你想访问大型文件的部分信息,却不想加载整个文件到内存时,您可以使用 GridFS 存储文件,并读取文件部分信息,而不需要加载整个文件到内存。
(3) 当你想让你的文件和元数据自动同步并部署在多个系统和设施,你可以使用 GridFS 实现分布式文件存储。
3. GridFS 存储原理
GridFS 使用两个集合(collection)存储文件。一个集合是 chunks, 用于存储文件内容的二进制数据;一个集合是 files,用于存储文件的元数据。
GridFS 会将两个集合放在一个普通的 buket 中,并且这两个集合使用 buket 的名字作为前缀。MongoDB 的 GridFs 默认使用 fs 命名的 buket 存放两个文件集合。因此存储文件的两个集合分别会命名为集合 fs.files , 集合 fs.chunks。
当然也可以定义不同的 buket 名字,甚至在一个数据库中定义多个 bukets,但所有的集合的名字都不得超过 mongoDB 命名空间的限制。
MongoDB 集合的命名包括了数据库名字与集合名字,会将数据库名与集合名通过“.”分隔 (eg:<database>.<collection>)。而且命名的最大长度不得超过 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”字段数据,还原文件。
4. 存储过程
fs.files 集合存储文件的元数据,以类 json 格式文档形式存储。每在 GridFS 存储一个文件,则会在 fs.files 集合中对应生成一个文档。
fs.files 集合中文档的存储内容如下:
fs.chunks 集合存储文件文件内容的二进制数据,以类 json 格式文档形式存储。每在 GridFS 存储一个文件,GridFS 就会将文件内容按照 chunksize 大小(chunk 容量为 256k)分成多个文件块,然后将文件块按照类 json 格式存在.chunks 集合中,每个文件块对应 fs.chunk 集合中一个文档。一个存储文件会对应一到多个 chunk 文档。
fs.chunks 集合中文档的存储内容如下:
为了提高检索速度 MongoDB 为 GridFS 的两个集合建立了索引。fs.files 集合使用是“filename”与“uploadDate” 字段作为唯一、复合索引。fs.chunk 集合使用的是“files_id”与“n”字段作为唯一、复合索引。
5. 注意事项
(1) GridFs 不会自动处理 md5 值相同的文件,也就是说,同一个文件进行两次 put 命令,将会在 GridFS 中对应两个不同的存储,对于存储来说,这是一种浪费。对于 md5 相同的文件,如果想要在 GridFS 中只有一个存储,需要通过 API 进行扩展处理。
(2) 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 正常使用下回收磁盘资源。
6. 代码示例
代码基于 spring boot,主要实现 GridFS 的基本操作。
(1) application.properties 配置如下:
spring.data.mongodb.uri=mongodb://localhost:27017/test
(2) Spring Boot 的启动函数
1 package com.ws; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 6 @SpringBootApplication 7 public class Application { 8 public static void main(String[] args) { 9 SpringApplication.run(Application.class, args); 10 } 11 }
(3) Spring Boot 的 domain 域,主要定义返回标识
1 package com.ws; 2 3 public class Response { 4 private String name; 5 6 public Response(String name) { 7 this.name = name; 8 } 9 10 public String getName() { 11 return name; 12 } 13 14 public void setName(String name) { 15 this.name = name; 16 } 17 18 }
(4) Spring Boot 的 Controller 层,定义接口函数
1 package com.ws; 2 3 import com.mongodb.BasicDBObject; 4 import com.mongodb.DBObject; 5 import com.mongodb.gridfs.GridFSDBFile; 6 import org.apache.commons.io.IOUtils; 7 import org.apache.log4j.Logger; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.data.mongodb.core.query.Criteria; 10 import org.springframework.data.mongodb.core.query.Query; 11 import org.springframework.data.mongodb.gridfs.GridFsTemplate; 12 import org.springframework.http.MediaType; 13 import org.springframework.web.bind.annotation.RequestMapping; 14 import org.springframework.web.bind.annotation.RequestMethod; 15 import org.springframework.web.bind.annotation.RequestParam; 16 import org.springframework.web.bind.annotation.RestController; 17 import org.springframework.web.multipart.MultipartFile; 18 19 import java.io.IOException; 20 import java.io.InputStream; 21 import java.util.Date; 22 import java.util.List; 23 import java.util.UUID; 24 25 @RestController 26 @RequestMapping("/api") 27 public class GridFSApi { 28 private static Logger LOGGER = Logger.getLogger(GridFSApi.class); 29 @Autowired 30 private GridFsTemplate gridFsTemplate; 31 32 @RequestMapping(value = "/save", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) 33 public Response save(@RequestParam(value = "file", required = true)MultipartFile file) { 34 35 LOGGER.info("Saving file.."); 36 DBObject metaData = new BasicDBObject(); 37 metaData.put("createdDate", new Date()); 38 39 String fileName = UUID.randomUUID().toString(); 40 41 LOGGER.info("File Name: " + fileName); 42 43 InputStream inputStream = null; 44 try { 45 inputStream = file.getInputStream(); 46 gridFsTemplate.store(inputStream, fileName, "image", metaData); 47 LOGGER.info("File saved: " + fileName); 48 } catch (IOException e) { 49 LOGGER.error("IOException: " + e); 50 throw new RuntimeException("System Exception while handling request"); 51 } 52 LOGGER.info("File return: " + fileName); 53 return new Response(fileName); 54 } 55 56 @RequestMapping(value = "/get", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE) 57 public byte[] get(@RequestParam(value = "fileName", required = true)String fileName) throws IOException { 58 LOGGER.info("Getting file.." + fileName); 59 List<GridFSDBFile> result = gridFsTemplate 60 .find(new Query().addCriteria(Criteria.where("filename").is(fileName))); 61 if (result == null || result.size()== 0) { 62 LOGGER.info("File not found" + fileName); 63 throw new RuntimeException("No file with name: " + fileName); 64 } 65 LOGGER.info("File found " + fileName); 66 return IOUtils.toByteArray(result.get(0).getInputStream()); 67 } 68 69 @RequestMapping(value = "/delete", method = RequestMethod.DELETE) 70 public void delete(@RequestParam(value = "fileName", required = true)String fileName) { 71 LOGGER.info("Deleting file.." + fileName); 72 gridFsTemplate.delete(new Query().addCriteria(Criteria.where("filename").is(fileName))); 73 LOGGER.info("File deleted " + fileName); 74 } 75 } 76