java导入大文件数据的解决方案

2018年11月5号于南昌 中海蓝域小区 卧榻伴音弦

最近在做项目,一个20G大小的文件,要按行读入到数据库,妈呀,有什么好方法吗?

20G如果按照行读入的方式,需要20多个小时才能入库成功。主要性能瓶颈不是在内存,而是在数据库连接的次数。

比如批量单次插入1000条和单次插入200条,其实是五倍性能差距。

但是这个批量插入的条数是根据数据库缓存大小设置而决定的,数据库缓存不可能太大,比如一个8G的服务器,数据库缓存建议最大200M等,单次插入条数最大上限是2000条每次,如果有5000w行数据,那么需要多少次数据连接呢?答案是:2.5万次连接,我天,可想而知,能不慢吗?连接数据库,每次都有通过网络访问,然后数据库有行级锁,再提交事务,单次连接如果1s,那么2.5万次连接就是
2.5万s,一小时是3600s,那么需要多少个小时呢

结果:2.5万除以0.36万,大概是7个小时。

大家知道了,上面并没有考虑读文件,放入内存的时间,算是这段时间,
时间就会更长,为什么忽略这个,因为在我看来,数据库连接所消耗的时间要远大于内存加载文件中的数据所消耗的时间。

分析问题要切中要点,忽略次要的因素。

大家脑海中可能想过以下几个方法:
原始方法如下:

@Override
	public void addDataFromBigDatFileOfScReturnRegister(String fileEndDate) throws IOException {
		String currentDate = fileEndDate;// DateUtils.formatDate(new Date(),
											// "yyyyMMdd");
		String dataFileFlagName = LoyParam.DOWN_LOCAL_PATH_BEFORE.concat(currentDate)
				.concat(LoyParam.DOWN_LOCAL_PATH_AFTER).concat(LoyParam.FILE_CIR_RETURN_REGISTER_).concat(currentDate)
				.concat(LoyParam.DOWN_FILENAME_FLG);

		String dataFileName = LoyParam.DOWN_LOCAL_PATH_BEFORE.concat(currentDate).concat(LoyParam.DOWN_LOCAL_PATH_AFTER)
				.concat(LoyParam.FILE_CIR_RETURN_REGISTER_).concat(currentDate).concat(LoyParam.DOWN_FILENAME_DAT);

		boolean flag_flg = FileUtil.isExistence(dataFileFlagName);
		boolean flag_dat = FileUtil.isExistence(dataFileName);
		if (!(flag_flg && flag_dat)) {
			// 其中一个不存在over
			return;
		}

		Map<String, String> fileMap = this.getDataFlagInfo(dataFileFlagName);
		if (fileMap.isEmpty()) {
			logger.info("要访问的文件不存在" + dataFileFlagName);
			return;
		}

		int datasize = 0;
		if (!StringCommonUtils.isEmpty(fileMap.get("dataSum"))) {
			datasize = Integer.parseInt(fileMap.get("dataSum"));
			logger.info("数据文件名为:" + fileMap.get("dataFile"));
			logger.info("数据文件大小为:" + fileMap.get("fileSize"));
			logger.info("数据总数为:" + datasize);
		}

		// 年月日时分秒
		String starttime = formatter.format(new Date());
		// 10位的年月日
		String currentY_M_D = formatterY_M_D.format(new Date());
		long start = System.currentTimeMillis();
		FileInputStream fis = new FileInputStream(dataFileName);
		Scanner sc = new Scanner(fis, "GBK");
		//创建备份表,插入备份表
		this.deleteAndCreateTmpTable(LoyParam.TABLE_SC_RETURN_REGISTER);

		int DB_COMMIT_COUNT=1000;
		int y = datasize % DB_COMMIT_COUNT;
		int inum = 0;
		List<SCT> ststlist = new ArrayList<SCT>();
		ScProblemRecordPojo problemPojo=null;
		List<SCT> problemList=new ArrayList<SCT>();
		ScReturnRegister scReturnRegister=null;
		while (sc.hasNextLine()) {
			String line = sc.nextLine();
			String[] data = line.split("\\x03");
			if (data.length > 0) {
				inum++;
				// logger.info("第"+inum+"行:"+line);
				Map<Integer, String> datamap = new HashMap<Integer, String>();
				for (int i = 0; i < data.length; i++) {
					String data_ = StringCommonUtils.replaceBlank(data[i]);
					datamap.put(i, data_);
				}
				try{
					String dataDate = StringFormat.trimNull(datamap.get(0));
					String loanNo = StringFormat.trimNull(datamap.get(1));
					String orgNo = StringFormat.trimNull(datamap.get(2));
					String custNo = StringFormat.trimNull(datamap.get(3));
					String custName = StringFormat.trimNull(datamap.get(4));
					String tranType = StringFormat.trimNull(datamap.get(5));
					String returnInt = StringFormat.trimNull(datamap.get(6));
					String returnAmt = StringFormat.trimNull(datamap.get(7));
					String tbdate = starttime.substring(0, 10);
	
					scReturnRegister = new ScReturnRegister();
					BeanUtil.setProperty(scReturnRegister, "dataDate", dataDate);
					BeanUtil.setProperty(scReturnRegister, "loanNo", loanNo);
					BeanUtil.setProperty(scReturnRegister, "orgNo", orgNo);
					BeanUtil.setProperty(scReturnRegister, "custNo", custNo);
					BeanUtil.setProperty(scReturnRegister, "custName", custName);
					BeanUtil.setProperty(scReturnRegister, "tranType", tranType);
					BeanUtil.setProperty(scReturnRegister, "returnInt", returnInt);
					BeanUtil.setProperty(scReturnRegister, "returnAmt", returnAmt);
					BeanUtil.setProperty(scReturnRegister, "tbdate", tbdate);
				}catch(Exception ex){
					logger.error("转换信息异常"+ex.getMessage());
					logger.info("========》我出错了 "+inum);
					problemPojo=new ScProblemRecordPojo(
							scReturnRegister==null?"":scReturnRegister.toString(),
									DateUtils.YYYYMMDD,
									DateUtils.YMDHMS,
									dataFileName,
									((Integer)inum).toString()
							);
					problemList.add(problemPojo);
					this.scCredTmTxnHstService.insertBatch(problemList, dataFileName);
					continue;
				}
				ststlist.add(scReturnRegister);
				datamap.clear();

				if (ststlist.size() == DB_COMMIT_COUNT) {
					scCredTmTxnHstService.insertBatch(ststlist,dataFileName);
					ststlist = null;
					ststlist = new ArrayList<SCT>();
					continue;
				}
				if (y > 0 && inum == datasize) {
					scCredTmTxnHstService.insertBatch(ststlist,dataFileName);
				    break;
				}
			}
		}
		logger.info("解析数据完成!共计" + inum + "条数据");
		sc.close();
		fis.close();
		long end = System.currentTimeMillis();
		System.out.println("时长为:---------------" + (start - end) + "毫秒---------------");
		if (inum == datasize) {
			FileOperate.delete(dataFileName);
			scEngineeEtlmxService.addScEngineeEtlmx(LoyParam.FILE_CIR_RETURN_REGISTER_, "数仓数据文件之"+dataFileName, starttime,
					formatter.format(new Date()), StringFormat.msecondToTime(end - start), currentY_M_D, datasize, inum,
					currentY_M_D, "Y");
			//如果成功做一系列处理
			this.insertDBSuccessProcessTable(LoyParam.TABLE_SC_RETURN_REGISTER);

		}

	}

代码很好理解,这里不多解释,主要性能问题大家也知道

		scCredTmTxnHstService.insertBatch(ststlist,dataFileName);

对,就在这里,将list数据,连接数据库,插入,提交事务,这个过程需要2.5万次,我天

考虑java解决问题的方式

1,多线程

多线程就是,将串行的方式,改成并行,比如:
排队去食堂打饭,就一个窗口,这是串行
排队去食堂打饭,但是有多个窗口共同执行,你可以在任何一个窗口排队,并行。

并行增加了效率,提高了打饭速度,随之而来的问题也很多。那就是并发问题。A人付款了,准备拿饭缸接住饭,这时候,由于并发,B人的没有付款,但是售货员把饭打到了B人的饭缸,造成了A付了款而没有收到饭,或者A可能收到了C人的饭,C收到了D的饭,乱套了。

解决这个方法,java当然也有解决方案,呵呵。

Synchronized神方法,不错,但是有一个问题,我们要对非原子的逻辑都要加Synchronized,所以方法块,或者干脆加到方法上。

言归正传,拥多线程并不能解决我们的问题,因为我们这个读取的方法,就这么一个,里面大多数都是非原子的逻辑,加载方法上,不管你启多少个线程,效果和串行一样,因为只有一个线程获取了锁,并且执行,而且速度也不快,所以这个方法解决不了问题

2,多进程
这个方法咋拥,其实就是把上面的方法,比如A 我们复制多个,分别叫
A A1 A2 A3
这里就有了4个方法,但是不能同时读取一个文件吧,这样数据会重复,有人会说数据库强加唯一索引,我说如果数据杂乱无章,没有主键呢?
这样就考虑将大文件切割,切割拥linux的split命令,见我下一篇文章,会有详细的解释

比如我们把文件F切割成 F1 F2 F3 F4

然后A方法读F1 入库
A1 读F2入库
A2 读F3入库
A3 读F4入库

这样我们可以启动4个进程,代码虽然缀余,但这不是我们讨论的重点,我们是讨论如何解决之,那么我们发现,这样的话,效率会增大,但是不会太明显,为什么,此时虽然说2.5万次连接,分了4个进程去连接,一个进程去执行2.5w除以4次连接,同时进行,相当于多线程,但他是多线程,
但为什么效果不明显,原因是数据库有行级锁,虽然同时连接的效率高了,但是对于数据库来说,处理插入是按照队列来的,是吧。

这样瓶颈就在数据库的行级锁,那么此时就没有办法了。

3 Channel 读取文件
这种方法,是java提供的读取同一个文件入库的多线程方法,大家有兴趣可以看一下,但是我没研究过,其实性能不在内存,而在数据库连接和数据库行级锁上面。

以上就是java导入大文件的解决方案,实际工作中的经验,分享之,愿大神有高见,指点一二,不胜感激。

======================
V。18612372242
励志语:
愿你我不忘初心,选一行爱一行,就像爱一个人终老一样

You are My love ForEver !

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知乎关注八戒来了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值