java 读取dbf文件_Java读取Level-1行情dbf文件极致优化(3)

本文介绍了针对Level-1行情DBF文件读取的优化方案,包括使用内存硬盘、JNotify、NIO、减少内存分配、字段读取优化和线程池并行处理,实现了从50ms-70ms的读取速度提升至4ms-7ms。
摘要由CSDN通过智能技术生成

最近架构一个项目,实现行情的接入和分发,需要达到极致的低时延特性,这对于证券系统是非常重要的。接入的行情源是可以配置,既可以是Level-1,也可以是Level-2或其他第三方的源。虽然Level-1行情没有Level-2快,但是作为系统支持的行情源,我们还是需要优化它,使得从文件读取,到用户通过socket收到行情,端到端的时延尽可能的低。本文主要介绍对level-1行情dbf文件读取的极致优化方案。相信对其他的dbf文件读取应该也有借鉴意义。

Level-1行情是由行情小站,定时每隔几秒把dbf文件(上海是show2003.dbf,深圳是sjshq.dbf)更新一遍,用新的行情替换掉旧的。我们的目标就是,在新文件完成更新后,在最短时间内将文件读取到内存,把每一行转化为对象,把每个列转化为对应的数据类型。

我们一共采用了6种优化方式。

优化一:采用内存硬盘(RamDisk)

优化二:采用JNotify,用通知替代轮询

优化三:采用NIO读取文件

优化四:减少读取文件时内存反复分配和GC

优化五:字段读取优化

行情dbf文件很多字段是价格类型的字段,带2位或者3位小数,从dbf读取他们的后,我们会把它们保存在Long类型或者Int类型,而不是Float或Double类型,比如1.23,转换为1230保存。因为Float型或Double型会丢失精度。

如果不优化,读取步骤为:

1,从byte[]对应的偏移中读取并保存到String中。

2,对String做trim操作

3,把String转换为Float类型

4,把Float类型乘以1000并强转为Long类型。

不用多说,以上的过程一定是低效的,光前两步就涉及到2次字符串拷贝,2次对象创建。第三步效率也不高。我这里通过优化,在DBFReader.java中添加一个get_long_efficiently_and_multiply_1000方法,将4个步骤合并为一步,通过一次扫描得到结果。

public long get_long_efficiently_and_multiply_1000(byte[] src, final intindex)

{long multiplicand = 3;long result =0;

Field field=getFields()[index];boolean in_decimal_part = false;boolean negative = false;int offset =field.getOffset();int length =field.getLength();int end = offset+length;for(int i =field.getOffset(); i< end; i++)

{byte ch =src[i];if(ch>=48 && ch<=57) //如果是数字

{

result*= 10;

result+= ch-48;if(in_decimal_part)

multiplicand--;if(multiplicand==0) break;continue;

}if(ch==32) //如果是空格

continue;if(ch == 46) //如果是小数点

{

in_decimal_part= true;continue;

}if(ch == '-') //如果是负号

{

negative= true;

}throw newNumberFormatException();

}if(multiplicand == 3)

result*= 1000;else if (multiplicand == 2)

result*=100;else if (multiplicand == 1)

result*=10;if(negative)

{

result= 0 -result;

}returnresult;

}

上面的算法负责读取字段转换为数字的同时,对它乘以1000。并且代码中尽量优化了执行步骤。

对于整形的读取,我们也进行了优化,添加一个get_long_efficiently:

public long get_long_efficiently(byte[] src, final intindex)

{long result =0;boolean negative = false;

Field field=getFields()[index];for(int i =field.getOffset(); i< field.getOffset()+ field.getLength(); i++)

{byte ch =src[i];if(ch>=48 && ch<=57) //如果是数字

{

result= result*10 + (src[i]-48);continue;

}if(src[i]==32) //如果是空格

continue;if(ch == '-') //如果是负号

{

negative= true;

}throw newNumberFormatException();

}if(negative)

{

result= 0 -result;

}returnresult;

}

以上的2个算法并不复杂,但却非常关键,一个dbf文件包含大约5000行,每行包括20~30个Float类型或者Int类型的字段,该优化涉及10万+个字段的读取。测试下来,这步改进将读取速度从50ms-70ms提升至15ms至20ms,细节在魔鬼当中,这是速度提升最快的一项优化。

(优化五的代码在改进的DBFReader中,上午中已经提供下载,这里再提供下载链接:a836216d731f923d9d176fb296c16d9f.pngDBFReader库 )

优化六:线程池并行处理

对5000多个行进行字段读取并转换成对象,采用多线程处理是最自然不过的优化方式。

一般我们采用的方法是把任务分成等份的块,每个线程处理一大块。比如,如果采用5个线程处理,那么把5000行分成1000个行一块,每个线程处理一块。这样看貌似公平,其实不然,因为我们的操作系统是分时操作系统,每个线程开始工作的时间,占用的CPU时间片,和任务的强度都不完全一致。等分的办法貌似平均,但是很有可能导致有些线程完成工作了,另外一些还有很多没做完。

这里介绍一种我喜欢的任务分配方式:每个线程每次从5000个行的任务中申请一小块,比如16个行,完成后,再申请16个行。这样快的线程就会多工作些,慢的就少工作些,直到所有的行处理完毕。那么,这些线程怎么协调呢,任务分配岂不是要用到锁?不用锁,我们采用CAS机制就能做到(实际用的是AtomicInteger,AtomicInteger就是基于CAS实现的),这里不解释太多了。看代码:

class ReaderTask implementsRunnable {

Collector collector;

ListrecordList;

CountDownLatch countDownLatch;

AtomicInteger cursor;

DBFReader reader;public ReaderTask(Collector collector, DBFReader dbfreader, ListrecordList, AtomicInteger cursor,

CountDownLatch countDownLatch) {this.collector =collector;this.reader =dbfreader;this.recordList =recordList;this.cursor =cursor;this.countDownLatch =countDownLatch;

}

@Overridepublic voidrun() {try{int length =recordList.size();do{final int step = 16; //每次分配16行给该线程处理。int endIndex =cursor.addAndGet(step);int startIndex = endIndex -step ;for (int i = startIndex; i < endIndex && i < length; i++) {byte[] row =recordList.get(i);

MarketRealtimeData SHData= newMarketRealtimeData();

SHData.setMarketType(Constants.MARKET_SH_STOCK);

SHData.setIdNum(reader.get_string_efficiently(row,0));

SHData.setPrefix(reader.get_string_efficiently(row,1));

SHData.setPreClosePrice(reader.get_long_efficiently_and_multiply_1000(row,2));

SHData.setOpenPrice(reader.get_long_efficiently_and_multiply_1000(row,3));

SHData.setTurnover(reader.get_long_efficiently_and_multiply_1000(row,4));

SHData.setHighPrice(reader.get_long_efficiently_and_multiply_1000(row,5));

SHData.setLowPrice(reader.get_long_efficiently_and_multiply_1000(row,6));

SHData.setMatchPrice(reader.get_long_efficiently_and_multiply_1000(row,7));//读取所有的Field,以下省略若干行//... ...//... ...

if (collector != null) {

collector.collect(SHData);

}

}

}while (cursor.get()

}finally{if (countDownLatch != null)

countDownLatch.countDown();

}

}

}

private void readHangqingFile(String path, String name) throwsException {//Long t1 = System.nanoTime();

DBFReader dbfreader_SH = null;try{

dbfreader_SH= new DBFReader(new File(path+File.separator +name));

List list_sh =dbfreader_SH.recordsWithOutDel_efficiently(cacheManager);

AtomicInteger cursor= new AtomicInteger(0); //原子变量,用于线程间分配任务

CountDownLatch countDownLatch = newCountDownLatch(WORK_THREAD_COUNT);for (int i = 0; i < WORK_THREAD_COUNT - 1; i++) { //把任务分配给线程池多个线程

ReaderTask task = newReaderTask(collector, dbfreader_SH, list_sh, cursor, countDownLatch);

globalExecutor.execute(task);

}new ReaderTask(collector, dbfreader_SH, list_sh, cursor, countDownLatch).run(); //当前线程自己也作为工作线程

countDownLatch.await();//Long t2 = System.nanoTime();//System.out.println("speed time on read and object:" + (t2 - t1));

}finally{if (dbfreader_SH != null)

dbfreader_SH.close();

}

}

测试表明,在使用4个线程并行处理的情况下,处理时间从15ms-20ms缩短至4ms-7ms。

在使用本文章介绍的所有优化方法,整个读取效率从耗时300ms以上,优化至5ms-10ms之间。我们讨论的是从文件更新始,到完成文件读取,完成5000多个对象,100,000个字段的转换的总耗时。

如果继续深入,我们可能还有不少细节可以改进。测试表明,时延的稳定性还不够好,很可能是由于GC造成的,我们还可以从减少对象的创建,以减少性能损耗,减少GC;并且控制GC执行的时间,让GC在空闲时执行等方面优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值