1、简介
首先说明,大文件的存储还是建议使用专业的OSS去做,这里只是单纯拓展技术,不涉及业务。(不谈业务聊技术是流氓行为!!)
业务场景如果有限制、项目技术架构不允许再扩展、文件的内存不是特别大等等的情况下(或者leader要求),只能使用数据库去存储大内存的二进制文件时,可以参考此篇文章。
OSS的英文全称是Object Storage Service,翻译成中文就是对象存储服务
2、模拟背景介绍
假设当前项目有一张表是存放公司员工信息的,现在leader想把员工的一些入职信息、简历、体检报告等等一系列的文件打成一个压缩包,预计每个员工的资料压缩包都有100M,现在不允许使用OSS服务去存储,因为只是一个小功能,以后基本不会用到这样的文件存储,所以只能存在数据库中,但是如果直接将压缩包使用IO流的方式将文件读取到内存中,再通过save的方式,可能会引发OOM,想象一下,如果将堆内存设置为500M,那我们在程序正常运行时可调取该方法进行存储文件的操作可以同时进行几个呢???三个或者四个就有可能导致程序直接被终止,虽然这种说法是极端情况,正常业务或技术设计也不会让这么浅显的问题被触发,这么描述当然是为了引出后面的解决方案啦~~
3、环境准备
-
准备一个Springboot项目,Mysql数据库
-
准备一些假的压缩包或文件,内存大小不重要,模拟为主
-
首先准备一张员工表,表结构如下
create table staff ( id varchar(72) not null comment '编号' primary key, name varchar(50) null comment '姓名', sex varchar(2) null comment '性别', age tinyint(100) null comment '年龄', data longblob null comment '资料' ) engine = InnoDB default charset = utf8mb4 row_format = dynamic;
注:longblob是Mysql用来存储大内存二进制数据的字段类型,具体可自行Google。
-
准备一个实体类
@Data @NoArgsConstructor @AllArgsConstructor @TableName(value = "staff") public class Staff implements Serializable { /** * 编号 */ private String id; /** * 姓名 */ private String name; /** * 性别 */ private String sex; /** * 年龄 */ private Integer age; /** * 资料 */ private byte[] data; }
4、问题解决
这里我只写一种做法,可以使用JDBC的方式进行实现,在存取时,可以使用JDBC提供的api直接用IO流写入数据库,上代码!!!
- 上传文件
/**
* 存
*/
@Test
void setBlob(){
// 创建一个员工对象
Staff staff = new Staff();
String id = UUID.randomUUID().toString().replace("-", "");
staff.setId(id);
staff.setName("Devin");
staff.setSex("男");
staff.setAge(18);
// 读取需要上传的文件
File file = new File(Objects.requireNonNull(Resource.class.getResource("/data/BlobAccessReadDemo.zip")).getFile());
try (
// 获取数据库连接
Connection connection = dataSource.getConnection();
// 获取sql对象
PreparedStatement pstmt = connection.prepareStatement(getInsertSql(tableName, staff));
// 获取文件输入流
FileInputStream fis = new FileInputStream(file);
) {
// 读取分段数据并追加到数据库字段中
pstmt.setBlob(1, fis);
pstmt.executeUpdate();
System.out.println("Data insert completed.");
} catch (SQLException | IOException e) {
log.error("insert error", e);
}
}
// 构造 INSERT SQL 语句
private static String getInsertSql(String tableName, Staff staff) {
return "INSERT INTO " + tableName + " (id, name, sex, age, data) VALUES "
+ "( '" + staff.getId() + "', '" + staff.getName() + "', '" + staff.getSex() + "'," + staff.getAge() + ", ?)" ;
}
- 下载文件
/**
* 取
*/
@Test
void getBlob(){
// 查询条件
String name = "Devin";
// 文件列名
String columnName = "data";
// 获取输出流
File outFile = new File(Objects.requireNonNull(Resource.class.getResource("/data")).getFile() + "BlobAccessWriteDemo.zip");
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(getSelectSql(tableName));
){
// 根据条件查询 将指定字段的文件数据写出到指定位置
ps.setString(1, name);
ps.executeQuery();
ResultSet rs = ps.getResultSet();
rs.next();
Blob blob = rs.getBlob(columnName);
InputStream is = blob.getBinaryStream();
FileUtils.copyInputStreamToFile(is, outFile);
System.out.println("Data select completed.");
} catch (Exception e) {
log.error("select error", e);
}
}
// 构造 SELECT SQL 语句
private static String getSelectSql(String tableName) {
return "SELECT * FROM " + tableName + " WHERE name = " + "?" ;
}
注:在写出文件时,如果直接用我的代码,不自己重写写出地址的话,会遇到找不到写到哪个目录的问题,我上面写的Resource.class.getResource
方法是获取resource目录的地址,程序运行编译时会将resource目录中的数据存放到target/classes目录下,如果写出没报错在这个目录下可以找到写出的文件
5、思考
-
分段写数据有一个问题,如果在从数据库读取数据时分段写出但还没完全写完时,程序挂了 or 网络不稳或断了,该怎么处理?
git代码中有处理方式,有兴趣的同学可以在思考后自行查看,注意解决方案不止一种,我的不是标准答案,只是个人思考,如有更好的方案,欢迎评论或私信,定当感激不尽!!!
6、总结
这类大文件存储的问题,不使用第三方平台存储本就不合理,但百兆说大也大,说小其实也小,但还是需要考虑到内存的问题,搞不好一个不小心程序G了就很尴尬。
git源码在这里:https://gitee.com/xiaokuncom/devin-blog.git
本次代码在blobAccess/test目录中,建表脚本在resources/sql目录下,测试用的zip在resource/data目录下
7、鸡汤送上
在所谓的人世间摸爬滚打至今,我唯一原意视为真理的,就只有这一句话。
一切都会过去的。
选自《人间失格》
最后说明
创作不易,若转载请标明出处或原文链接!!!
感觉写的还行的,帮忙点赞评论哦!!!