LinuxEye - Linux系统教程

LinuxEye - Linux系统教程

当前位置: 主页 > 数据库 >

基于Oracle的大数据导入方案探索

时间:2012-12-25 12:32来源:51CTO 编辑:xymrl123 点击:
数据导入功能在报表项目中是经常可见的,因为它是报表数据展示的基础,但对于大量数据的导入,真正从性能、效率等方面兼顾的方案却很少有。最近在南航广西数据服务平台的项目
数据导入功能在报表项目中是经常可见的,因为它是报表数据展示的基础,但对于大量数据的导入,真正从性能、效率等方面兼顾的方案却很少有。最近在南航广西数据服务平台的项目开发中,我需要设计一个能快速将40多万条数据导入Oracle数据库的方案,为了实现导入的高效,我通过在网上收集资料以及动手实践测试,得出了一些分析总结与大家分享探讨。

谈到数据导入功能的实现,无可厚非应该包括两个过程,首先是数据文件的上传,其次是数据的导入。

一、数据文件上传

文件上传本应该是与导入无关,但它处于数据导入功能的一个环节,其效率也显得有些重要。对于传统项目,文件上传通常采用Struts等框架实现的文件上传机制以及一些开源的文件上传组件,比如SmartUpload等,通过Html中类型为File的Input标签将数据文件获取,通过流的形式发送服务端,最后由服务端获取流并写入文件,如此实现了文件从客户端到服务器的上传过程,这些方式我们都可以将其统一称为Web文件上传。

Web文件上传是通过Http协议实现的文件流传输,其受限于Web数据传输的瓶颈,基于Http协议传输的数据在传输的速度上有一定影响,首先可能出现数据文件请求超时需要数据重传;其次在每秒能传输的字节数在Web方式里受到限制;最后,由于Web在传输数据文件之前需要将数据文件转化为数据流,通过Web的File标签实现的文件流化效率很低,通过对比试验发现,同样对一个50M的文件流化,用Web的File标签流化的时间要大于采用IO方式流化所消耗的时间,也就是说,对于大数据文件,要将其通过Web标签流化再传输,可能会有很长一段时间是处于发送请求状态,甚至会因为这个过程的时间持续较长而造成网络请求超时。这可能是优酷等类似的网站上传视频不采用Web方式的一个原因。

除了Web方式,可以考虑使用Applet,作为一个客户端小程序嵌入到网页中,以IO的方式读取本地数据文件,然后通过Socket将文件流发送到服务端。这种方式从效率上比Web方式有明显的改进,首先是通过IO将文件转化为文件流的效率提升,其次数据通过Socket方传输式,是一种基于TCP协议的网络传输,去除了Web下Http协议对文件传输的限制,通过TCP协议直接从网络的传输层进行数据通信,传输速度上必然会更快。综上,采用Applet加Socket实现网络文件上传性能优于Web方式。

然而,Applet实现的网络文件上传又并非最优,原因在于Applet在运行时受到沙箱的限制。出于对客户机和服务器的保护,web中的applet程序只能运行在限制的沙箱中,其受到很多安全策略的限制,在applet中不能直接访问客户端本地文件系统,除非使用applet授权,采用数字签名的方式使applet能确认该客户端系统是可信的。这样一来,要使用该功能的客户机系统都需要安装安全证书,在项目的部署上就显得十分繁琐。

以上方式都各有弊端,最终,通过和用户协商,我们决定借助外部的FTP工具,使用开源的文件传输工具让用户将数据文件直接传到服务器指定目录下,在网站系统上就只执行数据文件列表加载。另外,目前也有在web上嵌入FTP功能的插件,其通过activeObject的形式嵌入web,实现类似ftp的文件上传功能,打算抽空继续研究。

二、数据导入
大数据导入Oracle数据库是功能实现的重点。

大数据导入的特点在于数据记录多,数据插入需要批量执行,分批量commit,以此来减少对数据库的交互访问次数,减轻数据库压力。在此,我主要探索了两种导入方式,一种是基于多线程的并发插入,另一种是利用SqlLoader实现的大数据导入。

(一)基于多线程的并发插入

该方案是在批量到插入的基础之上采用多线程来执行的方式实现的。

该方案第一步是加载所需的数据文件到内存,生成一个Sql的数组。对于导入的数据文件,一般是EXCEL格式的,对于此种类型数据文件,我们需要借助POI来实现EXCEL文件的加载,并通过POI读取EXCEL中行数据来生成数据插入Sql,文件代码如下:
private XSSFSheet get07SheetForExcel(File file, String sheetName) {
if (file != null) {
try {
FileInputStream fileInputStream = new FileInputStream(file);
// 创建对Excel工作簿文件的引用
XSSFWorkbook workbook = new XSSFWorkbook(fileInputStream);
// 创建对工作表的引用
XSSFSheet sheet = workbook.getSheetAt(0);
// 也可用getSheetAt(int index)按索引引用,
// 在Excel文档中,第一张工作表的缺省索引是0$1�7
// 其语句为:HSSFSheet sheet = workbook.getSheetAt(0);
return sheet;
} catch (Exception e) {
}
}
return null;
}
                                 ……
for (int i = begin; null != sheet
&& i < sheet.getLastRowNum() - sheet.getFirstRowNum()
+ 1; i++) {
HSSFRow row = sheet.getRow(i);
System.out.println(i);
String[] valuesPerRow = getHSSFRowValues(row);
if (null == valuesPerRow || valuesPerRow.length < 1) {
continue;
}
rows.add(valuesPerRow);
if (rows.size() == CommonParas.PER_IMPORT_SIZE) {
v.importToDB(rows);
rows.clear();
}
}
v.importToDB(rows);

    这两个分别是POI加载Excel文件方法和读取文件行的数据的片段,从这里暴露出POI处理大数据EXCEL的问题所在。首先,是将EXCEL文件加载到XSSFSheet对象的一个过程需要消耗很多时间及内存资源,通过测试得出,当EXCEL数据记录在6万以上,文件大小超过30M时,此处就会出现异常“JVM内存溢出”,原因就是在加载EXCEL文件时开销太大了,即使我们在启动服务器时扩大JVM内存分配,这也只是治标不治本的方法;其次,在循环读取EXCEL行数据时需要预先获得EXCEL中总行数,而获得总行数的方法则是通过函数sheet.getLastRowNum() - sheet.getFirstRowNum()+ 1 实现,在此又出现了另一个问题,sheet.getLastRowNum()返回值是一个int类型的数据,其最大值只可能是65535,那么它又如何可以计算出超过65535行记录的行数呢?所以得出结论:对于大数据的读取,不能采用POI读取EXCEL的方式。

不采用EXCEL作为数据源文件,可以采用CSV文件代之。CSV是EXCEL可另存为的数据文件格式,其本质上是以逗号分隔的文本文件,因此,对于此类文件的读取,我们可以采用传统IO读取文件的形式,通过字符串分割获得每个单元格数据,拼接到SQL里面,形成SQL的数组。

     第二步,多线程执行数据导入。

     第一步已经将SQL储存到SQL的数组中,接下来就是利用多线程执行批量插入。此处实现需要将SQL执行类实现Runnable接口或者继承Thread类,在常量中设置批量插入的记录数,当加载的SQL数据大小达到该数目时,系统就自动启动一个线程执行该数组的批量数据导入。为了确保线程管理的安全,我们需要定义一个最大线程数,当处于运行的线程数达到最大值时,新开启的线程将利用wait()方法实现等待,等待有线程执行完毕后才开始执行。如此调用,直到加载的数据记录全部都已开始导入才停止线程的启动,此时为了监控数据导入是否全部完成,我们会再新开启一个后台线程,设置为守护进程setDaemon(true),该类线程特点是在所有线程结束后将自动结束,其用于收集每个导入线程的执行状态,当所分配的所有线程状态都不是active时就表示数据导入完成。

利用该方案实现的数据导入较单线程执行的批量数据导入效率提高多倍,从测试导入40万数据结果来看,单线程批量导入耗时19分钟,而基于多线程的导入只用了5分钟左右的时间。但从性能消耗上来看,多线程方案平均同时工作线程数为15个左右,CPU利用率高达90%,内存消耗约500M,对于服务器本身已造成了一定的压力,虽然在速度上提升了,其对于服务器的稳定性将造成安全隐患。

此外,对于多线程工作效率的探索上也有一点心得。多线程的出现更多的是迎合多核处理技术的革新,在单CPU工作的主机上,多线程看起来貌似是多个线程并发执行,但从操作系统的角度出发其仍然处于串行状态,因为在同一时间,处理器只对一个任务进行调度,只不过是轮询的时间间隙较短不容易发觉。如果在多核处理的主机上,就会有多个处理器同时处理并发的线程,这样才能实现真正意义上的并发调度,所以多线程还是依赖于硬件本身。为了验证效率,当我们把执行导入的各个线程以webService的形式部署到不同的虚拟机中去执行时,效果就不一样了,效率明显还会提升。由此引出一个当今IT行业的一个热点,虚拟化技术的实现与应用,有利于资源的优化配置,在有限的资源上实现更大的利用价值,该技术在云计算领域也是颇受关注的。

(二)利用SQLLOADER执行大数据导入

     SQLLOADER是oralce内置的一个命令工具,其可实现将CSV数据文件或者文本数据文件一次性导入数据库中。它的使用需要有两个文件,一个是需要导入的数据文件,另一个是控制导入的控制文件。该方案的执行需要三个步骤,第一是格式化数据文件,第二是生成控制文件,最后是执行导入命令。

第一,格式化数据文件。

数据文件不是已经上传了,为什么需要格式化呢?原因在于SQLLOADER对于导入数据文件要求的严格性。利用SQLLOADER导入的数据文件有如下要求:

1、数据行之间要有合法换行标志,数据与数据之间需要有合法分隔符,一般情况下数据之间用逗号分隔,数据记录之间用换行符分隔;

2、要导入的数据是直接导入到数据库的内容,不能包含表头信息,对于数据文件中有表头的需要将其去除;

3、数据中不能包含既定的数据分隔符,比如逗号,因为其在导入时对于数据个数不匹配的将视为错误数据被拒绝。

因此,为了确保数据能够准确的被导入到数据库,在数据导入之前我们需要验证原数据文件,并读取其数据信息生成格式化的新数据文件提供导入。这也许是这种导入方式繁琐的地方,但出于安全又不得不做。

第二,生成控制文件。

SQLLOADER执行数据导入并非直接通过命题操纵数据文件,而是通过一个控制文件规定了导入数据的方式,包括数据文件路径及导入数据字段等等,它通过执行该控制文件来实现数据的导入。下面来看一个控制文件内容。
load data                                                  
infile 'e:\test\舱位数据源(20120101-20120331).csv'   (1)
append into table CZDS_CABIN                           (2)
fields terminated by ','                                (3)
( CARRIER,                                                 (4)
  ORGCITY,
  ARRIVALCITY,
  FLYDATE "to_date(:FLYDATE,'YYYY-MM-DD')",
  FLIGHTNO,
  FLIGHTSEG,
  AIRLINE,
  AIRLINETOANDFROM,
  FLIGHTTYPE,
  ORGUNITS,
  MOTHERCABIN,
  SONSPACE,
  DISCOUNTFACTOR,
  CLASSES,
  FLIGHTNAVIGATIONSECTION,
  BOARDINGTIMES,
  GROUPTICKETRNUMBER,
  REVENUEFORECASTING,
  SEATINGSECTOR,
  SEATINGNAVIGATIONSECTION,
  SEATINGSONTNSECTION      )

(1)定义需要导入的格式化数据文件路径,可以是CSV文件或者TXT文本文件;

(2)定义导入数据库的方式为append,需要插入数据的表名为CZDS_CABIN;

(3)定义数据间的分隔符为逗号,此处,还可以定义更多的控制命令,比如是否允许插入空数据命令trailing nullcols等等。

(4)定义导入数据的对应的字段。这里所列的字段要与数据文件中每行数据中数据对应的列一致,因为SQLLOADER是按照这个顺序对数据进行插入的。其次,还有个值得注意的地方,在列名的后面可以用双引号定义导入后数据的格式。SQLLOADER导入数据时区分格式的,当插入数字和字符类型数据时可以根据文本进行自动转换,但是当插入数据是时间类型时,需要在此处定义格式转换,利用to_date方法将字符转化为日期再执行插入。同时,利用这个控制,可以实现对插入数据的格式化,比如利用lpad进行左填充等等,这也是SQLLOADER相对灵活的地方。

第三步,通过SQLLOADER命令执行数据导入。

这里的SQLLOADER命令是一个命令行命令,需要在JAVA程序中执行CMD程序来调用。
Process process = null;
try {
process = java.lang.Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}

Cmd就是需要执行的SQLLOADER命令。SQLLOADER命令如下:

sqlldr userid=mrl/mrl123456 control=e:\\test\\control1.txt log=aa.log  bad=bad.bad direct=true parallel=true

Userid指的是本地数据库的用户名和密码,不正确将连接不上数据库。

Control的值是我们之前生成的控制文件路径,命令将会执行该控制文件。

Log:这里定义了导入的日志文件路径。

Bad:当导入失败时,该文件里存放了没有成功导入的数据记录。

Direct:开启直接导入路径模式。

Parallel:开启并发导入模式。

在程序中执行此命令后,通过process.waitFor()方法来等待命令执行的返回,若返回值为0则说明程序正常结束,数据导入成功;如返回1,则说明数据导入中出现异常,需要排查日志文件,数据导入失败。

利用SQLLOADER执行数据导入的缺点在于其对数据文件格式要求的严格以及其只能导入本地数据库,而无法执行远程导入,且导入数据只能通过闪存回滚,存在风险性。但其优点又很明显,导入数据速度非常快,通过测试结果显示,导入40万数据仅用了7-8秒,较之前额方法可谓一个飞跃,这也许是其赢得大家一致认可的原因所在。

转载请保留固定链接: https://linuxeye.com/database/1090.html

------分隔线----------------------------
标签:Oracle大数据
栏目列表
推荐内容