最近架構一個項目,實現行情的接入和分發,須要達到極致的低時延特性,這對於證券系統是很是重要的。接入的行情源是能夠配置,既能夠是Level-1,也能夠是Level-2或其餘第三方的源。雖然Level-1行情沒有Level-2快,可是做爲系統支持的行情源,咱們仍是須要優化它,使得從文件讀取,到用戶經過socket收到行情,端到端的時延儘量的低。本文主要介紹對level-1行情dbf文件讀取的極致優化方案。相信對其餘的dbf文件讀取應該也有借鑑意義。 html
Level-1行情是由行情小站,定時每隔幾秒把dbf文件(上海是show2003.dbf,深圳是sjshq.dbf)更新一遍,用新的行情替換掉舊的。咱們的目標就是,在新文件完成更新後,在最短期內將文件讀取到內存,把每一行轉化爲對象,把每一個列轉化爲對應的數據類型。 java
咱們一共採用了6種優化方式。 數組
咱們在上文《Java讀取Level-1行情dbf文件極致優化(1)》中,介紹了2種咱們使用的優化策略:緩存
本文繼續介紹:性能優化
對於Dbf文件的讀寫,有許多的開源的實現,選擇和改進它們是這裏的重要策略。架構
有許多Dbf庫是基於流的I/O實現的,即InputStream和OutStream。咱們應該採用NIO的方式,即基於RandomAccessFile,FileChannel和ByteBuffer。流的方式是一邊處理數據,一邊從文件中讀取,而採用NIO能夠一次性把整個文件加載到內存中。有測試代表(見《Java程序性能優化》一書),NIO的方式大概比流的方式快5倍左右。我這裏提供採用NIO實現的dbf讀取庫供你們下載學習(最原始的出處已不可考了。這個代碼被改寫了,其中也已經包含我以後將要提出的優化策略),若是你的項目已經有dbf庫,建議基於本文的優化策略進行改進,而不是直接替換爲我提供的。dom
DBFReader庫socket
其中,DBFReader.java中有以下代碼片斷:性能
建立FileChannel代碼爲:學習
this.dbf = new RandomAccessFile(file, "r"); this.fc = dbf.getChannel();
把指定的文件片斷加載到ByteBuffer的代碼爲
private ByteBuffer loadData(int offset, int length) throws IOException { // return fc.map(MapMode.READ_ONLY, offset, length).load(); ByteBuffer b = ByteBuffer.allocateDirect(length); fc.position(offset); fc.read(b); b.rewind(); return b; }
以上,咱們使用ByteBuffer.allocateDirect(length)建立ByteBuffer。 allocateDirect方法建立的是DirectBuffer,DirectBuffer分配在」內核緩存區」,比普通的ByteBuffer快一倍,這也有利於咱們程序的優化。可是DirectBuffer的建立和銷燬更耗時,在咱們接下來的優化中將要解決這一問題。
(我不打算詳細介紹NIO的相關知識(可能我也講不清楚),也不打算詳細介紹DbfReader.java的代碼,只重點講解和性能相關的部分,接下來也是如此。)
以上我提供的DBFReader.java文件讀取的文件的基本步驟是 :
1,把整個文件(除了文件頭)讀取到ByteBuffer當中(其實爲DirectBuffer)
2,再把每一行從ByteBuffer讀取到一個個byte[]數組中。
3,把這些byte[]數組封裝在一個一個Record對象中(Record對象提供了從byte[]中讀取列的各類方法)。
見如下loadRecordsWithOutDel方法:
private List<Record> loadRecordsWithOutDel() throws IOException { ByteBuffer bb = loadData(getDataIndex(), getCount() * getRecordLength()); List<Record> rds = new ArrayList<Record>(getCount()); for (int i = 0; i < getCount(); i++) { byte[] b = new byte[getRecordLength()]; bb.get(b); if ((char) b[0] != '*') { Record r = new Record(b); rds.add(r); } } bb.clear(); return rds; }
private ByteBuffer loadData(int offset, int length) throws IOException { // return fc.map(MapMode.READ_ONLY, offset, length).load(); ByteBuffer b = ByteBuffer.allocateDirect(length); fc.position(offset); fc.read(b); b.rewind(); return b; }
考慮到咱們系統的實際應用的狀況:行情dbf文件每隔幾秒就會刷新一遍,刷新後的大小基本上差很少,格式是徹底同樣的,每行的大小是同樣的。
注意看以上代碼中高亮的部分,會反覆建立ByteBuffer和byte數組。在咱們的應用場景下,徹底可使用一種緩存機制來重複使用他們,避免反覆建立。要知道一個行情文件有5000多行之多,避免如此之多的new和GC,確定對性能有好處。
我添加了一個CacheManager類來完成這個工做:
import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; public class CacheManager { private ByteBuffer byteBuffer = null; private int bufSize = 0; private List<byte[]> byteArrayList = null; private int bytesSize = 0; public CacheManager() { } public ByteBuffer getByteBuffer(int size) { if(this.bufSize < size) { byteBuffer = ByteBuffer.allocateDirect(size + 1024*8); //多分配一些,避免下次從新分配 this.bufSize = size + 1024*8; } byteBuffer.clear(); return byteBuffer; } public List<byte[]> getByteArrayList(int rowNum, int byteLength) //rowNum爲行數,即須要的byte[]數量,byteLength爲byte數組的大小 { if(this.bytesSize!=byteLength) { byteArrayList = new ArrayList<byte[]>(); this.bytesSize = byteLength; } if(byteArrayList.size() < rowNum) { int shouldAddRowCount = rowNum - byteArrayList.size()+100; //多分配100行 for(int i=0; i<shouldAddRowCount; i++) { byteArrayList.add(new byte[bytesSize]); } } return byteArrayList; } }
CacheManager 管理了一個能夠反覆使用的ByteBuffer,以及能夠反覆使用的byte[]列表。
其中,getByteBuffer方法用於返回一個緩存的ByteBuffer。其只有當緩存的ByteBuffer小於指定的大小時,才從新建立ByteBuffer。(爲了儘可能避免這種狀況,咱們老是分配比實際須要大一些的ByteBuffer)。
其中,getByteArrayList方法用於返回緩存的byte[]列表。其只有當須要的Byte[]數量小於須要的數量時,建立更多的byte[]; 若是緩存的byte[]們的長度和須要的不符,就從新建立全部的byte[](這種狀況不可能發生,由於每行的大小不會變,代碼只是以防萬一而已)。
將loadRecordsWithOutDel改造爲recordsWithOutDel_efficiently,採用緩存機制:
public List<byte[]> recordsWithOutDel_efficiently(CacheManager cacheManager) throws IOException { ByteBuffer bb = cacheManager.getByteBuffer(getCount() * getRecordLength()); fc.position(getDataIndex()); fc.read(bb); bb.rewind(); List<byte[]> rds = new ArrayList<byte[]>(getCount()); List<byte[]> byteArrayList = cacheManager.getByteArrayList(getCount(), getRecordLength()); for (int i = 0; i < getCount(); i++) { byte[] b = byteArrayList.get(i); bb.get(b); if ((char) b[0] != '*') { rds.add(b); } } bb.clear(); return rds; }
在新的recordsWithOutDel_efficiently中,咱們從CacheManager中分配緩存的ByteBuffer和緩存的byte[]。而不是從系統分配,從而減小了反覆的內存分配和GC。(另外,recordsWithOutDel_efficiently直接返回byte[]列表,而不是Record列表了)
個人測試發現,優化步驟四,即便用緩存的方式,大概把時間從5ms左右降到了2ms多,提升大概一倍。
到此,咱們只是完成了文件到內存的讀取。接着是爲每一行建立一個行情對象,從byte[]中把每一列數據讀取出來。 我發現,其耗時遠遠超過文件讀取,在沒有優化的狀況下,對5000多行數據的轉換超過70ms。這是咱們接下來須要介紹的優化策略。
待續。。。
Binhua Liu原創文章,轉載請註明原地址http://www.cnblogs.com/Binhua-Liu/p/5615299.html