众所周知java在处理数据量比较大的时候加载到内存必然会导致内存溢出而在一些数据处理中我们不得不去处理海量数据在做数据处理中我们常见的手段是分解压缩并行临时文件等方法
例如我们要将数据库(不论是什么数据库)的数据导出到一个文件一般是Excel或文本格式的CSV对于Excel来讲对于POI和JXL的接口你很多时候没有办法去控制内存什么时候向磁盘写入很恶心而且这些API在内存构造的对象大小将比数据原有的大小要大很多倍数所以你不得不去拆分Excel还好POI开始意识到这个问题在的版本后开始提供cache的行数提供了SXSSFWorkbook的接口可以设置在内存中的行数不过可惜的是他当你超过这个行数每添加一行它就将相对行数前面的一行写入磁盘(如你设置行的话当你写第行的时候他会将第一行写入磁盘)其实这个时候他些的临时文件以至于不消耗内存不过这样你会发现刷磁盘的频率会非常高我们的确不想这样因为我们想让他达到一个范围一次性将数据刷如磁盘比如一次刷M之类的做法可惜现在还没有这种API很痛苦我自己做过测试通过写小的Excel比使用目前提供刷磁盘的API来写大文件效率要高一些而且这样如果访问的人稍微多一些磁盘IO可能会扛不住因为IO资源是非常有限的所以还是拆文件才是上策而当我们写CSV也就是文本类型的文件我们很多时候是可以自己控制的不过你不要用CSV自己提供的API也是不太可控的CSV本身就是文本文件你按照文本格式写入即可被CSV识别出来如何写入呢?下面来说说
在处理数据层面如从数据库中读取数据生成本地文件写代码为了方便我们未必要M怎么来处理这个交给底层的驱动程序去拆分对于我们的程序来讲我们认为它是连续写即可我们比如想将一个W数据的数据库表导出到文件此时你要么进行分页oracle当然用三层包装即可mysql用limit不过分页每次都会新的查询而且随着翻页会越来越慢其实我们想拿到一个句柄然后向下游动编译一部分数据(如行)将写文件一次(写文件细节不多说了这个是最基本的)需要注意的时候每次buffer的数据在用outputstream写入的时候最好flush一下将缓沖区清空下接下来执行一个没有where条件的SQL会不会将内存撑爆?是的这个问题我们值得去思考下通过API发现可以对SQL进行一些操作例如通过PreparedStatement statement = connectionprepareStatement(sql)这是默认得到的预编译还可以通过设置
PreparedStatement statement = connectionprepareStatement(sqlResultSetTYPE_FORWARD_ONLYResultSetCONCUR_READ_ONLY)
来设置游标的方式以至于游标不是将数据直接cache到本地内存然后通过设置statementsetFetchSize()设置游标每次遍历的大小OK这个其实我用过oracle用了和没用没区别因为oracle的jdbc API默认就是不会将数据cache到java的内存中的而mysql里头设置根本无效我上面说了一堆废话呵呵我只是想说java提供的标准API也未必有效很多时候要看厂商的实现机制还有这个设置是很多网上说有效的但是这纯属抄袭对于oracle上面说了不用关心他本身就不是cache到内存所以java内存不会导致什么问题如果是mysql首先必须使用以上的版本然后在连接参数上加上useCursorFetch=true这个参数至于游标大小可以通过连接参数上加上defaultFetchSize=来设置例如
jdbcmysql//xxxxxxxxxxxx/abc?zeroDateTimeconvertToNull&useCursorFetch=true&defaultFetchSize=< /span>
上次被这个问题纠结了很久(mysql的数据老导致程序内存膨胀并行个直接系统就宕了)还去看了很多源码才发现奇迹竟然在这里最后经过mysql文档的确认然后进行测试并行多个而且数据量都是W以上的都不会导致内存膨胀GC一切正常这个问题终于完结了
我们再聊聊其他的数据拆分和合并当数据文件多的时候我们想合并当文件太大想要拆分合并和拆分的过程也会遇到类似的问题还好这个在我们可控制的范围内如果文件中的数据最终是可以组织的那么在拆分和合并的时候此时就不要按照数据逻辑行数来做了因为行数最终你需要解释数据本身来判定但是只是做拆分是没有必要的你需要的是做二进制处理在这个二进制处理过程你要注意了和平时read文件不要使用一样的方式平时大多对一个文件读取只是用一次read操作如果对于大文件内存肯定直接挂掉了不用多说你此时因该每次读取一个可控范围的数据read方法提供了重载的offset和length的范围这个在循环过程中自己可以计算出来写入大文件和上面一样不要读取到一定程序就要通过写入流flush到磁盘其实对于小数据量的处理在现代的NIO技术的中也有用到例如多个终端同时请求一个大文件下载例如视频下载吧在常规的情况下如果用java的容器来处理一般会发生两种情况
其一为内存溢出因为每个请求都要加载一个文件大小的内存甚至于更多因为java包装的时候会产生很多其他的内存开销如果使用二进制会产生得少一些而且在经过输入输出流的过程中还会经历几次内存拷贝当然如果有你类似nginx之类的中间件那么你可以通过send_file模式发送出去但是如果你要用程序来处理的时候内存除非你足够大但是java内存再大也会有GC的时候如果你内存真的很大GC的时候死定了当然这个地方也可以考虑自己通过直接内存的调用和释放来实现不过要求剩余的物理内存也足够大才行那么足够大是多大呢?这个不好说要看文件本身的大小和访问的频率
其二为假如内存足够大无限制大那么此时的限制就是线程传统的IO模型是线程是一个请求一个线程这个线程从主线程从线程池中分配后就开始工作经过你的Context包装Filter拦截器业务代码各个层次和业务逻辑访问数据库访问文件渲染结果等等其实整个过程线程都是被挂住的所以这部分资源非常有限而且如果是大文件操作是属于IO密集型的操作大量的CPU时间是空余的方法最直接当然是增加线程数来控制当然内存足够大也有足够的空间来申请线程池不过一般来讲一个进程的线程池一般会受到限制也不建议太多的而在有限的系统资源下要提高性能我们开始有了new IO技术也就是NIO技术新版的里面又有了AIO技术NIO只能算是异步IO但是在中间读写过程仍然是阻塞的(也就是在真正的读写过程但是不会去关心中途的响应)还未做到真正的异步IO在监听connect的时候他是不需要很多线程参与的有单独的线程去处理连接也又传统的socket变成了selector对于不需要进行数据处理的是无需分配线程处理的而AIO通过了一种所谓的回调注册来完成当然还需要OS的支持当会掉的时候会去分配线程目前还不是很成熟性能最多和NIO吃平不过随着技术发展AIO必然会超越NIO目前谷歌V虚拟机引擎所驱动的nodejs就是类似的模式有关这种技术不是本文的说明重点
将上面两者结合起来就是要解决大文件还要并行度最土的方法是将文件每次请求的大小降低到一定程度如K(这个大小是经过测试后网络传输较为适宜的大小本地读取文件并不需要这么小)如果再做深入一些可以做一定程度的cache将多个请求的一样的文件cache在内存或分布式缓存中你不用将整个文件cache在内存中将近期使用的cache几秒左右即可或你可以采用一些热点的算法来配合类似迅雷下载的断点传送中(不过迅雷的网络协议不太一样)它在处理下载数据的时候未必是连续的只要最终能合并即可在服务器端可以反过来谁正好需要这块的数据就给它就可以才用NIO后可以支持很大的连接和并发本地通过NIO做socket连接测试个终端同时请求一个线程的服务器正常的WEB应用是第一个文件没有发送完成第二个请求要么等待要么超时要么直接拒绝得不到连接改成NIO后此时个请求都能连接上服务器端服务端只需要个线程来处理数据就可以将很多数据传递给这些连接请求资源每次读取一部分数据传递出去不过可以计算的是在总体长连接传输过程中总体效率并不会提升只是相对相应和所开销的内存得到量化控制这就是技术的魅力也许不要太多的算法不过你得懂他
类似的数据处理还有很多有些时候还会将就效率问题比如在HBase的文件拆分和合并过程中要不影响线上业务是比较难的事情很多问题值得我们去研究场景因为不同的场景有不同的方法去解决但是大同小异明白思想和方法明白内存和体系架构明白你所面临的是沈阳的场景只是细节上改变可以带来惊人的效果