文章目录
开发背景
需求背景
由于以前的文件存在于SQLServer数据表中,而这种对文件的保存方式是十份不友好的,比如说这个项目中的file
数据表就是这种情况,由于在其中存入了近10G的文件,这导致该表数据量十份臃肿,比如说我们执行一条简单的SQL,如下:
Select * From file
由于数据量过大,该条查询语句往往会执行上半天时间,如果再多执行几次这种文件查询操作,系统就直接卡死,因而原来的这种文件存储方式是存在巨大的风险的,所以说我需要将存储于表中的数据文件导出成真正的文件,以此来解决现今所面临的问题。
分析解决
从上面我们可以看出,整个的业务逻辑是比较的简单的,就是将文件从数据表中读出,然后写成真正的文件。因而这里就无需多做说明。
这里需要重点说说其技术实现。
由于上述表中存在大量的信息,单纯的查询操作往往会导致程序卡死,并且一次性的读取上G的文件数据本身也并不合理 ,因而我这里采用的是分页查询。也就是说通过先统计该表信息的总条数,然后循环遍历,每次读取10条,直至全部数据读取完毕。
至于为什么每次读取10条,其主要是经过测试之后得出的,测试数据如下:
file表:
文件总数:12224
当前文件数:180
当前文件总容量:550M
时间:1小时10分即70分钟
最大文件大小:30M
平均文件大小:
550M/180=3.05M
平均每小时生成文件容量:
550M/(70/60)=471.43M
预估文件总大小:
12224*3.05=37283.2M=36.4G
预估总共用时:
37283.2M/471.43M=79小时
即file表生成完毕,耗时需要:79/34=3.29天
通过初步测试,我们得出,在该表中,最大的单个文件为30M,平均为3.05M,而一次性读取的数据,控制在100M以内是比较合理的,如果一次性读取过多,会加重系统内存以及数据IO负担,而一次性读取过少,会导致很多时间浪费在查询上,并不合理,所以说最终我将一次性读取的信息条数定为10条。平均每次读取30.5M的数据,最大读取300M数据量。
由于不同的文件表有不同的规则,比如说案卷附件表,需要在导出附件文件的同时关联查出文件编号,最终将附件文件放在案卷编号文件夹下;而封面、详情页等属于文件表,则需要关联文件表,读取文件名称(类似于书名),将附件文件放入到文件名称文件夹下。
换言之,不同的附件文件表拥有不同的规则(也就是函数),这对于我来说是一个新的挑战,在这里,我采用的是函数式编程(即使用@FunctionalInterface
注解),通过在遍历文件时动态的传入附件文件函数,以此来确定该附件文件到底执行什么样的导出方法。
源码
核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- sqlserver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
Java源码
工具类
PathUtil
package com.lyc.sqlserver.util;
import com.lyc.sqlserver.common.CommonConstant;
import com.lyc.sqlserver.entity.ProjectPath;
import java.util.Map;
/**
* @author: zhangzhenyi
* @date: 2019/4/20 13:48
* @description: 文件路径工具类
**/
public class PathUtil {
/**
* 返回PathUtil实例
* @return
*/
public static PathUtil newPathUtil(){
return new PathUtil();
}
/**
* 获取文件完整路径
* @param file 文件对象
* @param folderPath 文件夹路径
* @return
*/
public String getFileFullPath(Map<String,Object> file, String folderPath) {
// 获取文件路径
String filePath = getFilePath(file);
// 返回文件完整路径
return folderPath
+ CommonConstant.Separators.BACK_SLASH
+ filePath;
}
/**
* 获取文件路径
* @param file 文件对象
* @return
*/
private String getFilePath(Map<String,Object> file) {
// 返回文件路径
return getString(file.get("fname"));
}
/**
* 获取文件夹路径
* @param pathMap 项目路径集合
* @return
*/
public String getFolderPath(Map<String,Object> pathMap) {
// 文件夹路径 = 系统绝对路径 + 项目名 + 数据表名 + 档案编号
return CommonConstant.ProjectPathName.SYSTEM_PATH
+ CommonConstant.Separators.BACK_SLASH
+ getProjectName(pathMap.get(CommonConstant.ProjectPropertyName.PROJECT))
+ CommonConstant.Separators.BACK_SLASH
+ getString(pathMap.get(CommonConstant.ProjectPropertyName.TABLE));
}
/**
* 获取项目名
* @param o
* @return
*/
public String getProjectName(Object o) {
ProjectPath projectPath = (ProjectPath) o;
return projectPath.getName();
}
/**
* 将对象转换成字符串
* @param o 文件夹名称
* @return
*/
public String getString(Object o) {
return String.valueOf(o);
}
/**
* 获取默认的文件完整路径
* @param fileName
* @param folderPath
* @return
*/
public String getFileFullPathDefault(String fileName, String folderPath) {
// 返回文件完整路径
return folderPath
+ CommonConstant.Separators.BACK_SLASH
+ fileName;
}
}
FileUtil
package com.lyc.sqlserver.util;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author: zhangzhenyi
* @date: 2019/4/20 14:39
* @description: 文件工具类
**/
@Slf4j
public class FileUtil {
/**
* 返回创建的FileUtil对象
* @return
*/
public static FileUtil newFileUtil(){
return new FileUtil();
}
/**
* 获取文件字节码并返回
* @param fileContent 字节码对象
* @return
*/
public byte[] getByte(Object fileContent) {
if(fileContent instanceof byte[]){
return (byte[]) fileContent;
}
// 如果不存在,就创建一个空对象,防止空指针异常现象的发生
return new byte[0];
}
/**
* 生成附件文件
* @param fileFullPath 文件全路径
* @param fileByteArray 文件字节流
*/
public void generateFile(String fileFullPath, byte[] fileByteArray) {
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(fileFullPath);
// 读取的字节对象
for(byte eleb : fileByteArray){
outputStream.write(eleb);
}
} catch (IOException e) {
log.error("生成文件出错!故障码:{}",e);
e.printStackTrace();
} finally {
// 获取文件输入流
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("文件流关闭出错!故障码:{}",e);
e.printStackTrace();
}
}
}
/**
* 如果文件夹路径不存在,则创建
* @param fileFullPath 文件夹全路径
*/
public void generateFolder(String fileFullPath) {
File folder = new File(fileFullPath);
while(!folder.exists()){
folder.mkdirs();
}
}
}
FieldFormatUtil
package com.lyc.sqlserver.util;
import com.google.common.collect.Maps;
import lombok.var;
import java.util.Date;
import java.util.Map;
/**
* @author: zhangzhenyi
* @date: 2019/3/29 11:22
* @description: Map<String,Object> 类型的数据格式化成对应的格式并返回
**/
public class FieldFormatUtil {
private final Map<String,Object> map;
private final DataConvertUtil dataConvertUtil;
/**
* FieldFormatUtil 构造函数
* @param map
*/
public FieldFormatUtil(Map<String,Object> map) {
if(null != map) {
// 如果不为空,则获取数据
this.map = map;
} else {
// 否则设置一个默认的空对象
this.map = Maps.newHashMap();
}
dataConvertUtil = DataConvertUtil.newDataConvertUtil();
}
/**
* 创建一个FieldFormatUtil 对象
* @param map
* @return
*/
public static FieldFormatUtil newFieldFormatUtil(Map<String,Object> map){
return new FieldFormatUtil(map);
}
/**
* 获取整型数据
* @param k
* @return
*/
public int getInt(String k){
var obj = this.map.get(k);
return dataConvertUtil.getInt(obj);
}
/**
* 获取Long型数据
* @param k
* @return
*/
public Long getLong(String k) {
var obj = this.map.get(k);
return dataConvertUtil.getLong(obj);
}
/**
* 获取String型数据
* @param k
* @return
*/
public String getString(String k) {
var obj = this.map.get(k);
return dataConvertUtil.getString(obj);
}
/**
* 获取Double类型的数据
* @param k
* @return
*/
public Double getDouble(String k) {
var obj = this.map.get(k);
return dataConvertUtil.getDouble(obj);
}
/**
* 获取时间类型的数据
* @param k
* @return
*/
public Date getDate(String k) {
var obj = this.map.get(k);
return dataConvertUtil.getDate(obj);
}
/**
* 获取double类型的数据
* @param k
* @return
*/
public float getFloat(String k) {
var obj = this.map.get(k);
return dataConvertUtil.getFloat(obj);
}
}
DataConvertUtil
package com.lyc.sqlserver.util;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import java.util.Date;
/**
* @author: zhangzhenyi
* @date: 2019/3/29 11:37
* @description: 数据类型转换工具
**/