最近业务方有一个需求,需要一次导入超过100万数据到系统数据库。可能大家首先会想,这么大的数据,干嘛通过程序去实现导入,为什么不直接通过SQL导入到数据库。

大数据量报表导出请参考:Java实现大批量数据导入导出(100W以上) -(二)导出

一、为什么一定要在代码实现

说说为什么不能通过SQL直接导入到数据库,而是通过程序实现:

1. 首先,这个导入功能开始提供页面导入,只是开始业务方保证的一次只有<3W的数据导入;

2. 其次,业务方导入的内容需要做校验,比如门店号,商品号等是否系统存在,需要程序校验;

3. 最后,业务方导入的都是编码,数据库中还要存入对应名称,方便后期查询,SQL导入也是无法实现的。

基于以上上三点,就无法直接通过SQL语句导入数据库。那就只能老老实实的想办法通过程序实现。

二、程序实现有以下技术难点

1. 一次读取这么大的数据量,肯定会导致服务器内存溢出;

2. 调用接口保存一次传输数据量太大,网络传输压力会很大;

3. 最终通过SQL一次批量插入,对数据库压力也比较大,如果业务同时操作这个表数据,很容易造成死锁。

三、解决思路

根据列举的技术难点我的解决思路是:

1. 既然一次读取整个导入文件,那就先将文件流上传到服务器磁盘,然后分批从磁盘读取(支持多线程读取),这样就防止内存溢出;

2. 调用插入数据库接口也是根据分批读取的内容进行调用;

3. 分批插入数据到数据库。

四、具体实现代码

1. 流式上传文件到服务器磁盘

 略,一般Java上传就可以实现,这里就不贴出。

2. 多线程分批从磁盘读取

批量读取文件:

 import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; /**
* 类功能描述:批量读取文件
*
* @author WangXueXing create at 19-3-14 下午6:47
* @version 1.0.0
*/
public class BatchReadFile {
private final Logger LOGGER = LoggerFactory.getLogger(BatchReadFile.class);
/**
* 字符集UTF-8
*/
public static final String CHARSET_UTF8 = "UTF-8";
/**
* 字符集GBK
*/
public static final String CHARSET_GBK = "GBK";
/**
* 字符集gb2312
*/
public static final String CHARSET_GB2312 = "gb2312";
/**
* 文件内容分割符-逗号
*/
public static final String SEPARATOR_COMMA = ","; private int bufSize = 1024;
// 换行符
private byte key = "\n".getBytes()[0];
// 当前行数
private long lineNum = 0;
// 文件编码,默认为gb2312
private String encode = CHARSET_GB2312;
// 具体业务逻辑监听器
private ReaderFileListener readerListener; public void setEncode(String encode) {
this.encode = encode;
} public void setReaderListener(ReaderFileListener readerListener) {
this.readerListener = readerListener;
} /**
* 获取准确开始位置
* @param file
* @param position
* @return
* @throws Exception
*/
public long getStartNum(File file, long position) throws Exception {
long startNum = position;
FileChannel fcin = new RandomAccessFile(file, "r").getChannel();
fcin.position(position);
try {
int cache = 1024;
ByteBuffer rBuffer = ByteBuffer.allocate(cache);
// 每次读取的内容
byte[] bs = new byte[cache];
// 缓存
byte[] tempBs = new byte[0];
while (fcin.read(rBuffer) != -1) {
int rSize = rBuffer.position();
rBuffer.rewind();
rBuffer.get(bs);
rBuffer.clear();
byte[] newStrByte = bs;
// 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面
if (null != tempBs) {
int tL = tempBs.length;
newStrByte = new byte[rSize + tL];
System.arraycopy(tempBs, 0, newStrByte, 0, tL);
System.arraycopy(bs, 0, newStrByte, tL, rSize);
}
// 获取开始位置之后的第一个换行符
int endIndex = indexOf(newStrByte, 0);
if (endIndex != -1) {
return startNum + endIndex;
}
tempBs = substring(newStrByte, 0, newStrByte.length);
startNum += 1024;
}
} finally {
fcin.close();
}
return position;
} /**
* 从设置的开始位置读取文件,一直到结束为止。如果 end设置为负数,刚读取到文件末尾
* @param fullPath
* @param start
* @param end
* @throws Exception
*/
public void readFileByLine(String fullPath, long start, long end) throws Exception {
File fin = new File(fullPath);
if (!fin.exists()) {
throw new FileNotFoundException("没有找到文件:" + fullPath);
}
FileChannel fileChannel = new RandomAccessFile(fin, "r").getChannel();
fileChannel.position(start);
try {
ByteBuffer rBuffer = ByteBuffer.allocate(bufSize);
// 每次读取的内容
byte[] bs = new byte[bufSize];
// 缓存
byte[] tempBs = new byte[0];
String line;
// 当前读取文件位置
long nowCur = start;
while (fileChannel.read(rBuffer) != -1) {
int rSize = rBuffer.position();
rBuffer.rewind();
rBuffer.get(bs);
rBuffer.clear();
byte[] newStrByte;
//去掉表头
if(nowCur == start){
int firstLineIndex = indexOf(bs, 0);
int newByteLenth = bs.length-firstLineIndex-1;
newStrByte = new byte[newByteLenth];
System.arraycopy(bs, firstLineIndex+1, newStrByte, 0, newByteLenth);
} else {
newStrByte = bs;
} // 如果发现有上次未读完的缓存,则将它加到当前读取的内容前面
if (null != tempBs && tempBs.length != 0) {
int tL = tempBs.length;
newStrByte = new byte[rSize + tL];
System.arraycopy(tempBs, 0, newStrByte, 0, tL);
System.arraycopy(bs, 0, newStrByte, tL, rSize);
}
// 是否已经读到最后一位
boolean isEnd = false;
nowCur += bufSize;
// 如果当前读取的位数已经比设置的结束位置大的时候,将读取的内容截取到设置的结束位置
if (end > 0 && nowCur > end) {
// 缓存长度 - 当前已经读取位数 - 最后位数
int l = newStrByte.length - (int) (nowCur - end);
newStrByte = substring(newStrByte, 0, l);
isEnd = true;
}
int fromIndex = 0;
int endIndex = 0;
// 每次读一行内容,以 key(默认为\n) 作为结束符
while ((endIndex = indexOf(newStrByte, fromIndex)) != -1) {
byte[] bLine = substring(newStrByte, fromIndex, endIndex);
line = new String(bLine, 0, bLine.length, encode);
lineNum++;
// 输出一行内容,处理方式由调用方提供
readerListener.outLine(line.trim(), lineNum, false);
fromIndex = endIndex + 1;
}
// 将未读取完成的内容放到缓存中
tempBs = substring(newStrByte, fromIndex, newStrByte.length);
if (isEnd) {
break;
}
}
// 将剩下的最后内容作为一行,输出,并指明这是最后一行
String lineStr = new String(tempBs, 0, tempBs.length, encode);
readerListener.outLine(lineStr.trim(), lineNum, true);
} finally {
fileChannel.close();
fin.deleteOnExit();
}
} /**
* 查找一个byte[]从指定位置之后的一个换行符位置
*
* @param src
* @param fromIndex
* @return
* @throws Exception
*/
private int indexOf(byte[] src, int fromIndex) throws Exception {
for (int i = fromIndex; i < src.length; i++) {
if (src[i] == key) {
return i;
}
}
return -1;
} /**
* 从指定开始位置读取一个byte[]直到指定结束位置为止生成一个全新的byte[]
*
* @param src
* @param fromIndex
* @param endIndex
* @return
* @throws Exception
*/
private byte[] substring(byte[] src, int fromIndex, int endIndex) throws Exception {
int size = endIndex - fromIndex;
byte[] ret = new byte[size];
System.arraycopy(src, fromIndex, ret, 0, size);
return ret;
}
}

以上是关键代码:利用FileChannel与ByteBuffer从磁盘中分批读取数据

多线程调用批量读取:

 /**
* 类功能描述: 线程读取文件
*
* @author WangXueXing create at 19-3-14 下午6:51
* @version 1.0.0
*/
public class ReadFileThread extends Thread {
private ReaderFileListener processDataListeners;
private String filePath;
private long start;
private long end;
private Thread preThread; public ReadFileThread(ReaderFileListener processDataListeners,
long start,long end,
String file) {
this(processDataListeners, start, end, file, null);
} public ReadFileThread(ReaderFileListener processDataListeners,
long start,long end,
String file,
Thread preThread) {
this.setName(this.getName()+"-ReadFileThread");
this.start = start;
this.end = end;
this.filePath = file;
this.processDataListeners = processDataListeners;
this.preThread = preThread;
} @Override
public void run() {
BatchReadFile readFile = new BatchReadFile();
readFile.setReaderListener(processDataListeners);
readFile.setEncode(processDataListeners.getEncode());
try {
readFile.readFileByLine(filePath, start, end + 1);
if(this.preThread != null){
this.preThread.join();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

监听读取:

 import java.util.ArrayList;
import java.util.List; /**
* 类功能描述:读文件监听父类
*
* @author WangXueXing create at 19-3-14 下午6:52
* @version 1.0.0
*/
public abstract class ReaderFileListener<T> {
// 一次读取行数,默认为1000
private int readColNum = 1000; /**
* 文件编码
*/
private String encode; /**
* 分批读取行列表
*/
private List<String> rowList = new ArrayList<>(); /**
*其他参数
*/
private T otherParams; /**
* 每读取到一行数据,添加到缓存中
* @param lineStr 读取到的数据
* @param lineNum 行号
* @param over 是否读取完成
* @throws Exception
*/
public void outLine(String lineStr, long lineNum, boolean over) throws Exception {
if(null != lineStr && !lineStr.trim().equals("")){
rowList.add(lineStr);
} if (!over && (lineNum % readColNum == 0)) {
output(rowList);
rowList = new ArrayList<>();
} else if (over) {
output(rowList);
rowList = new ArrayList<>();
}
} /**
* 批量输出
*
* @param stringList
* @throws Exception
*/
public abstract void output(List<String> stringList) throws Exception; /**
* 设置一次读取行数
* @param readColNum
*/
protected void setReadColNum(int readColNum) {
this.readColNum = readColNum;
} public String getEncode() {
return encode;
} public void setEncode(String encode) {
this.encode = encode;
} public T getOtherParams() {
return otherParams;
} public void setOtherParams(T otherParams) {
this.otherParams = otherParams;
} public List<String> getRowList() {
return rowList;
} public void setRowList(List<String> rowList) {
this.rowList = rowList;
}
}

实现监听读取并分批调用插入数据接口:

 import com.today.api.finance.ImportServiceClient;
import com.today.api.finance.request.ImportRequest;
import com.today.api.finance.response.ImportResponse;
import com.today.api.finance.service.ImportService;
import com.today.common.Constants;
import com.today.domain.StaffSimpInfo;
import com.today.util.EmailUtil;
import com.today.util.UserSessionHelper;
import com.today.util.readfile.ReadFile;
import com.today.util.readfile.ReadFileThread;
import com.today.util.readfile.ReaderFileListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.FutureTask;
import java.util.stream.Collectors; /**
* 类功能描述:报表导入服务实现
*
* @author WangXueXing create at 19-3-19 下午1:43
* @version 1.0.0
*/
@Service
public class ImportReportServiceImpl extends ReaderFileListener<ImportRequest> {
private final Logger LOGGER = LoggerFactory.getLogger(ImportReportServiceImpl.class);
@Value("${READ_COL_NUM_ONCE}")
private String readColNum;
@Value("${REPORT_IMPORT_RECEIVER}")
private String reportImportReceiver;
/**
* 财务报表导入接口
*/
private ImportService service = new ImportServiceClient(); /**
* 读取文件内容
* @param file
*/
public void readTxt(File file, ImportRequest importRequest) throws Exception {
this.setOtherParams(importRequest);
ReadFile readFile = new ReadFile();
try(FileInputStream fis = new FileInputStream(file)){
int available = fis.available();
long maxThreadNum = 3L;
// 线程粗略开始位置
long i = available / maxThreadNum; this.setRowList(new ArrayList<>());
StaffSimpInfo staffSimpInfo = ((StaffSimpInfo)UserSessionHelper.getCurrentUserInfo().getData());
String finalReportReceiver = getEmail(staffSimpInfo.getEmail(), reportImportReceiver);
this.setReadColNum(Integer.parseInt(readColNum));
this.setEncode(ReadFile.CHARSET_GB2312);
//这里单独使用一个线程是为了当maxThreadNum大于1的时候,统一管理这些线程
new Thread(()->{
Thread preThread = null;
FutureTask futureTask = null ;
try {
for (long j = 0; j < maxThreadNum; j++) {
//计算精确开始位置
long startNum = j == 0 ? 0 : readFile.getStartNum(file, i * j);
long endNum = j + 1 < maxThreadNum ? readFile.getStartNum(file, i * (j + 1)) : -2L; //具体监听实现
preThread = new ReadFileThread(this, startNum, endNum, file.getPath(), preThread);
futureTask = new FutureTask(preThread, new Object());
futureTask.run();
}
if(futureTask.get() != null) {
EmailUtil.sendEmail(EmailUtil.REPORT_IMPORT_EMAIL_PREFIX, finalReportReceiver, "导入报表成功", "导入报表成功" ); //todo 等文案
}
} catch (Exception e){
futureTask.cancel(true);
try {
EmailUtil.sendEmail(EmailUtil.REPORT_IMPORT_EMAIL_PREFIX, finalReportReceiver, "导入报表失败", e.getMessage());
} catch (Exception e1){
//ignore
LOGGER.error("发送邮件失败", e1);
}
LOGGER.error("导入报表类型:"+importRequest.getReportType()+"失败", e);
} finally {
futureTask.cancel(true);
}
}).start();
}
} private String getEmail(String infoEmail, String reportImportReceiver){
if(StringUtils.isEmpty(infoEmail)){
return reportImportReceiver;
}
return infoEmail;
} /**
* 每批次调用导入接口
* @param stringList
* @throws Exception
*/
@Override
public void output(List<String> stringList) throws Exception {
ImportRequest importRequest = this.getOtherParams();
List<List<String>> dataList = stringList.stream()
.map(x->Arrays.asList(x.split(ReadFile.SEPARATOR_COMMA)).stream().map(String::trim).collect(Collectors.toList()))
.collect(Collectors.toList());
LOGGER.info("上传数据:{}", dataList);
importRequest.setDataList(dataList);
// LOGGER.info("request对象:{}",importRequest, "request增加请求字段:{}", importRequest.data);
ImportResponse importResponse = service.batchImport(importRequest);
LOGGER.info("===========SUCESS_CODE======="+importResponse.getCode());
//导入错误,输出错误信息
if(!Constants.SUCESS_CODE.equals(importResponse.getCode())){
LOGGER.error("导入报表类型:"+importRequest.getReportType()+"失败","返回码为:", importResponse.getCode() ,"返回信息:",importResponse.getMessage());
throw new RuntimeException("导入报表类型:"+importRequest.getReportType()+"失败"+"返回码为:"+ importResponse.getCode() +"返回信息:"+importResponse.getMessage());
}
// if(importResponse.data != null && importResponse.data.get().get("batchImportFlag")!=null) {
// LOGGER.info("eywa-service请求batchImportFlag不为空");
// }
importRequest.setData(importResponse.data); }
}
注意:
第53行代码:
long maxThreadNum = 3L;
就是设置分批读取磁盘文件的线程数,我设置为3,大家不要设置太大,不然多个线程读取到内存,也会造成服务器内存溢出。

以上所有批次的批量读取并调用插入接口都成功发送邮件通知给导入人,任何一个批次失败直接发送失败邮件。

数据库分批插入数据:

   /**
* 批量插入非联机第三方导入账单
* @param dataList
*/
def insertNonOnlinePayment(dataList: List[NonOnlineSourceData]) : Unit = {
if (dataList.nonEmpty) {
CheckAccountDataSource.mysqlData.withConnection { conn =>
val sql =
s""" INSERT INTO t_pay_source_data
(store_code,
store_name,
source_date,
order_type,
trade_type,
third_party_payment_no,
business_type,
business_amount,
trade_time,
created_at,
updated_at)
VALUES (?,?,?,?,?,?,?,?,?,NOW(),NOW())""" conn.setAutoCommit(false)
var stmt = conn.prepareStatement(sql)
var i = 0
dataList.foreach { x =>
stmt.setString(1, x.storeCode)
stmt.setString(2, x.storeName)
stmt.setString(3, x.sourceDate)
stmt.setInt(4, x.orderType)
stmt.setInt(5, x.tradeType)
stmt.setString(6, x.tradeNo)
stmt.setInt(7, x.businessType)
stmt.setBigDecimal(8, x.businessAmount.underlying())
stmt.setString(9, x.tradeTime.getOrElse(null))
stmt.addBatch()
if ((i % 5000 == 0) && (i != 0)) { //分批提交
stmt.executeBatch
conn.commit
conn.setAutoCommit(false)
stmt = conn.prepareStatement(sql) }
i += 1
}
stmt.executeBatch()
conn.commit()
}
}
}

 以上代码实现每5000 行提交一次批量插入,防止一次提较数据库的压力。

以上,如果大家有更好方案,请留言。

Java实现大批量数据导入导出(100W以上) -(一)导入的更多相关文章

  1. Java实现大批量数据导入导出(100W以上) -(二)导出

    使用POI或JXLS导出大数据量(百万级)Excel报表常常面临两个问题: 1. 服务器内存溢出: 2. 一次从数据库查询出这么大数据,查询缓慢. 当然也可以分页查询出数据,分别生成多个Excel打包 ...

  2. Java实现大批量数据导入导出(100W以上) -(三)超过25列Excel导出

    前面一篇文章介绍大数据量导出实现: Java实现大批量数据导入导出(100W以上) -(二)导出 这篇文章在Excel列较少时,按以上实际验证能很快实现生成.但如果列较多时用StringTemplat ...

  3. sqlserver自带的导入导出工具,分别导入大批量mysql和oracle数据时的感受

    sqlserver自带的导入导出工具,分别导入大批量mysql和oracle数据时,mysql经常出现格式转换出错,不好导入  导入的数据量比较大时,还不如自己写个工具导入 今天在导oracle时,想 ...

  4. 实现excel导入导出功能,excel导入数据到页面中,页面数据导出生成excel文件

    今天接到项目中的一个功能,要实现excel的导入,导出功能.这个看起来思路比较清楚,但是做起了就遇到了不少问题. 不过核心的问题,大家也不会遇到了.每个项目前台页面,以及数据填充方式都不一样,不过大多 ...

  5. OpenXml Excel数据导入导出(含图片的导入导出)

    声明:里面的很多东西是基于前人的基础上实现的,具体是哪些人 俺忘了,我做了一些整合和加工 这个项目居于openxml做Excel的导入导出,可以用OpenXml读取Excel中的图片 和OpenXml ...

  6. linux导入导出数据库方法 windows导入导出数据库方法

    1.使用管理员账号(sys)登录查询字符集信息 第一步:查询LinuxOracle数据库的字符集 select userenv('language') from dual; 查询结果集可能为:AMER ...

  7. SQL Server 之 在数据库之间进行数据导入导出

    1.同一服务器上数据库之间进行数据导入导出 (1).使用 SELECT INTO 导出数据 在SQL Server中使用最广泛的就是通过SELECT INTO语句导出数据,SELECT INTO语句同 ...

  8. 2.11 Hive中数据导入导出Import和Export使用

    https://cwiki.apache.org/confluence/display/Hive/LanguageManual+ImportExport 一.Export.Import Export ...

  9. 大批量数据导出excel

    有次面试时,老板问我大批量数据一次性导出会有什么问题 感谢度娘提供,感谢原博主提供 https://www.cnblogs.com/zou90512/p/3989450.html

随机推荐

  1. Java并发-线程安全性

    首先了解一下多线程的概念 多线程:两段或以上的代码同时进行,多个顺序执行流. 并发和并行的区别 并发:做一下这个做一下那个. 并行:同时进行. 线程和进程的区别 进程:资源分配的基本单位,运行中的程序 ...

  2. manifold tangent classifier

    The Manifold Tangent Classifier (MTC) Putting it all together, here is the high level summary of how ...

  3. Java结合SpringBoot拦截器实现简单的登录认证模块

    Java结合SpringBoot拦截器实现简单的登录认证模块 之前在做项目时需要实现一个简单的登录认证的功能,就寻思着使用Spring Boot的拦截器来实现,在此记录一下我的整个实现过程,源码见文章 ...

  4. Python_字符串之删除空白字符或某字符或字符串

    ''' strip().rstrip().lstrip()分别用来删除两端.右端.左端.连续的空白字符或字符集 ''' s='abc ' s2=s.strip() #删除空白字符 print(s2) ...

  5. @Scheduled cron表达式

    一.Cron详解: Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式: 1.Seconds Minutes Hours Dayof ...

  6. python 关于操作文件的相关模块(os,sys,shutil,subprocess,configparser)

    一:os模块 os模块提供了许多允许你程序与操作系统直接交互的功能 os.getcwd() 获取当前工作目录,即当前python脚本工作的目录路径 os.chdir("dirname&quo ...

  7. Spring3.1 对Bean Validation规范的新支持(方法级别验证)

    上接Spring提供的BeanPostProcessor的扩展点-1继续学习. 一.Bean Validation框架简介 写道Bean Validation standardizes constra ...

  8. 搭建微信小程序服务

    准备域名和证书 任务时间:20min ~ 40min 小程序后台服务需要通过 HTTPS 访问,在实验开始之前,我们要准备域名和 SSL 证书. 域名注册 如果您还没有域名,可以在腾讯云上选购,过程可 ...

  9. CSS透明opacity和IE各版本透明度滤镜filter的准确用法

    滤镜名    说明 Alpha     让HTML元件呈现出透明的渐进效果Blur     让HTML元件产生风吹模糊的效果Chroma     让图像中的某一颜色变成透明色DropShadow    ...

  10. 用 150 行 Python 代码写的量子计算模拟器

    简评:让你更轻松地明白,量子计算机如何遵循线性代数计算的. 这是个 GItHub 项目,可以简单了解一下. qusim.py 是一个多量子位的量子计算机模拟器(玩具?),用 150 行的 python ...