Java讀取Level-1行情dbf文件極致優化(2)

最近架構一個項目,實現行情的接入和分發,須要達到極致的低時延特性,這對於證券系統是很是重要的。接入的行情源是能夠配置,既能夠是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種咱們使用的優化策略:緩存

優化一:採用內存硬盤(RamDisk)

優化二:採用JNotify,用通知替代輪詢

 

本文繼續介紹:性能優化

 

優化三:採用NIO讀取文件

對於Dbf文件的讀寫,有許多的開源的實現,選擇和改進它們是這裏的重要策略。架構

 

有許多Dbf庫是基於流的I/O實現的,即InputStream和OutStream。咱們應該採用NIO的方式,即基於RandomAccessFile,FileChannel和ByteBuffer。流的方式是一邊處理數據,一邊從文件中讀取,而採用NIO能夠一次性把整個文件加載到內存中。有測試代表(見《Java程序性能優化》一書),NIO的方式大概比流的方式快5倍左右。我這裏提供採用NIO實現的dbf讀取庫供你們下載學習(最原始的出處已不可考了。這個代碼被改寫了,其中也已經包含我以後將要提出的優化策略),若是你的項目已經有dbf庫,建議基於本文的優化策略進行改進,而不是直接替換爲我提供的。dom

download2DBFReader庫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的代碼,只重點講解和性能相關的部分,接下來也是如此。)

 

優化四:減小讀取文件時內存反覆分配和GC

以上我提供的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

相關文章
相關標籤/搜索