Lucene總結系列(三)--總述優化方案和呈現實時內存索引實現(結合RAMDirectory源碼解析)

前兩篇講清楚基礎和基本api調用,接下來我們就是要進入優化篇章了。

本系列:

(1)SSM框架構建積分系統和基本商品檢索系統(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(1)框架整合構建

(2)SSM框架構建積分系統和基本商品檢索系統(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(2)建立商品數據庫和Lucene的搭建

(3)Redis系列(一)--安裝、helloworld以及讀懂配置文件

(4)Redis系列(二)--緩存設計(整表緩存以及排行榜緩存方案實現)

(5) Lucene總結系列(一)--認識、helloworld以及基本的api操作。

(6)Lucene總結系列(二)--商品檢索系統的文字檢索業務(lucene項目使用)


文章結構:(1)總述優化方案;(2)呈現實時內存目錄索引(結合源碼解析);


一、總述優化方案:

(1)索引創建優化:

1. 先將索引寫入RAMDirectory,再批量寫 入FSDirectory,不管怎樣,目的都是盡量少的文件IO,因為創建索引的最大瓶頸在于磁盤IO。

2. 通過設置IndexWriter的參數優化索引建立

IndexWriter的forceMerge方法。當小文件達到多少個時,就自動合并多個小文件為一個大文件,因為它的使用代價較高不意見使用此方法,默認情況下lucene會自己合并。合并cfs文件。比如設定10,就是當小文件達到10個就自動合并成一個索引cfs文件。(而且只能在close前一步使用)

打開 IndexWriter 的時候,設置 autoCommit = false同傳統的數據庫操作一樣,批量提交事務性能總是比每個操作一個事務的性能能好很多。

3.在建立索引過程中,使用單例的 IndexWriter基于內存執行 Flush 而不是基于 document count--也就是內存消耗flush代替文檔數量flush。indexWriter可以自動根據內存消耗調用flush()。可以使用indexWriterConfig.setRAMBufferSizeMB(double)設置緩沖區大小。測試表明48MB為叫合適值。

4. 重用Document和Field。創建Document單一實例,使用Field的setValue方法重用Field。而通過 setValue 實現,這將有助于更有效的減少GC開銷而改善性能。

5.創建單例的IndexWriter。

6.關閉復合文件格式(Compound file format)調用setUseCompoundFile(false),可以關閉。建立復合文件,將可能使得索引建立時間被拉長,有可能達到7%-33%。而關閉復合文件格式,將可能大大增加文件數量,而由于減少了文件合并操作,索引性能被明顯增強。

7.不要使用太多的小字段,如果字段過多,嘗試將字段合并到一個更大的字段中,以便于查詢和索引適當增加 mergeFactor,但是不要增加的太多。關閉所有不需要的特性使用更快的 Analyzer特別是對于中文分詞而言,分詞器對于性能的影響更加明顯。

(2)搜索索引時優化:

1. 建立實時內存索引,將索引放入內存(注意:針對數量小型的索引,當索引大于1G就要考慮分布式索引了)

通過RAMDirectory內存讀寫緩寫提高性能

2. 合適使用api選擇適合的范圍索引:

[一]RangeQuery范圍搜索。設置范圍,但是RangeQuery的實現實際上是將時間范圍內的時間點展開,組成一個個BooleanClause加入 到 BooleanQuery中查詢,因此時間范圍不可能設置太大,經測試,范圍超過一個月就會拋 BooleanQuery.TooManyClauses,可以通過設 置 BooleanQuery.setMaxClauseCount(int maxClauseCount)擴大,但是擴大也是有限的,并且隨著 maxClauseCount擴大,占用內存也擴大。

[二]RangeFilter替代。用RangeFilter代替RangeQuery,經測試速度不會比RangeQuery慢,但是仍然有性能瓶頸,查詢的90%以上時間耗費在 RangeFilter,研究其源碼發現RangeFilter實際上是首先遍歷所有索引,生成一個BitSet,標記每個document,在時間范圍內的標記為true,不在的標記為false,然后將結果傳遞給Searcher查找,這是十分耗時的。

針對Filter再進一步的優化:

[1]緩存Filter結果。既然RangeFilter的執行是在搜索之前,那么它的輸入都是一定的,就是IndexReader, 而 IndexReader是由Directory決定的,所以可以認為RangeFilter的結果是由范圍的上下限決定的,也就是由具體 的 RangeFilter對象決定,所以我們只要以RangeFilter對象為鍵,將filter結果BitSet緩存起來即可。 lucene API已經提供了一個CachingWrapperFilter類封裝了Filter及其結果,所以具體實施起來我們可以 cache CachingWrapperFilter對象,需要注意的是,不要被CachingWrapperFilter的名字及其說明誤 導, CachingWrapperFilter看起來是有緩存功能,但的緩存是針對同一個filter的,也就是在你用同一個filter過濾不 同 IndexReader時,它可以幫你緩存不同IndexReader的結果,而我們的需求恰恰相反,我們是用不同filter過濾同一 個 IndexReader,所以只能把它作為一個封裝類。

[2]降低時間精度。研究Filter的工作原理可以看出,它每次工作都是遍歷整個索引的,所以時間粒度越大,對比越快,搜索時間越短,在不影響功能的情況下,時間精度越低越好,有時甚至犧牲一點精度也值得,當然最好的情況是根本不作時間限制。

針對上面的優化例子:
第一組,時間精度為秒:方式 直接用RangeFilter 使用cache 不用filter 。平均每個線程耗時 10s 1s 300ms
第二組,時間精度為天:方式 直接用RangeFilter 使用cache 不用filter。平均每個線程耗時 900ms 360ms 300ms。

所以:

盡量降低時間精度,將精度由秒換成天帶來的性能提高甚至比使用cache還好,最好不使用filter。

在不能降低時間精度的情況下,使用cache能帶了10倍左右的性能提高。

3.IndexSearcher單例化。

(3)其余零散的優化點:

1. 使用最新版本的Lucene。使用本地文件系統(盡量不使用虛擬機),使用更快的硬件設備,尤其是SSD。

2. 使用更快的分析器。主要是對磁盤空間的優化,可以將索引文件減小將近一半,相同測試數據下由600M減少到380M。但是對時間并沒有什么幫助,甚至會需要更長時 間,因為較好的分析器需要匹配詞庫,會消耗更多cpu

3. 關鍵詞區分大小寫。or AND TO等關鍵詞是區分大小寫的,lucene只認大寫的,小寫的當做普通單詞。

4.設置boost。有些時候在搜索時某個字段的權重需要大一些,例如你可能認為標題中出現關鍵詞的文章比正文中出現關鍵詞的文章更有價值,你可以把標題的boost設置的更大,那么搜索結果會優先顯示標題中出現關鍵詞的文章(沒有使用排序的前題下)。使用方法:Field. setBoost(float boost);默認值是1.0,也就是說要增加權重的需要設置得比1大。

部分參考

方案大致列舉這些,然后后面的文章會以這個為根結點不斷去擴散的,并且結合源碼解讀下進一步的優化方案。


二、呈現實時內存索引(結合源碼解析):

(1)代碼實現以及優化效果展示:

第一次索引平均時間(無內存索引)

這里寫圖片描述

第一次索引平均時間(無內存索引)

這里寫圖片描述

第一次索引平均時間(建立內存索引)。

這里寫圖片描述

第一次索引平均時間(建立內存索引)。比無內存快一倍多。

這里寫圖片描述
//查看我們demo工程的LuceneUtil類
 static {
        try {
            directory_sp = FSDirectory.open(new File(Constant.INDEXURL_ALL));
            matchVersion = Version.LUCENE_44;
            analyzer = new IKAnalyzer();
            config = new IndexWriterConfig(matchVersion, analyzer);
            System.out.println("directory_sp    " + directory_sp);
            // 創建內存索引庫,讓硬盤的庫交給內存庫
            ramDirectory = new RAMDirectory(directory_sp, null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

  public static IndexSearcher getIndexSearcherOfSP() throws IOException {

        System.out.println("directory_sp    " + directory_sp);
        //打開的是內存庫
        IndexReader indexReader = DirectoryReader.open(ramDirectory);

        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        return indexSearcher;
    }

(2)源碼分析:

RAMDirectory類是一個駐留內存的(memory-resident)Directory抽象類的實現。

public class RAMDirectory extends Directory {
    /**
   * 存放了一個fileName 和 RAMFile的鍵值對。
   */
    protected final Map<String, RAMFile> fileMap;
    protected final AtomicLong sizeInBytes;//jdk1.8才有的
/*
    初始化時:
    LockFactory抽象類的一個具體實現類SingleInstanceLockFactory。SingleInstanceLockFactory類的特點是,所有的加鎖操作必須通過該SingleInstanceLockFactory的一個實例而發生,也就是說,在進行加鎖操作的時候,必須獲取到這個SingleInstanceLockFactory的實例。
    
*/
    public RAMDirectory() {
        this.fileMap = new ConcurrentHashMap();//線程安全
        this.sizeInBytes = new AtomicLong();

        try {
        //實際上,在獲取到一個SingleInstanceLockFactory的實例后,那么對該目錄Directory進行的所有的鎖都已經獲取到,這些鎖都被存放到SingleInstanceLockFactory類定義的locks中。
            this.setLockFactory(new SingleInstanceLockFactory());
        } catch (IOException var2) {
            ;
        }

    }
/*
  * 僅僅當硬盤中的索引能全部放入內存中的時候才能調用此方法,它會將所有的現有Index放入到內存中來。。也就是索引比較小的時候才用的方案。大概小于1G。否則得話將會發生OOM的異常。
  * 通過這種方法得到的RAMDirectory對象是一個獨立于以前的directory對象的新的索引對象
  * 對于以前的Directory對象的任何修改都不會對新的RAMDirectory對象造成影響
  * 因為新的對象中包含所有的已有index的文件信息
*/
    public RAMDirectory(Directory dir, IOContext context) throws IOException {
        this(dir, false, context);
    }

    private RAMDirectory(Directory dir, boolean closeDir, IOContext context) throws IOException {
        this();//先初始化fileMap,并加鎖
        String[] arr$ = dir.listAll();//存放索引名字
        int len$ = arr$.length;

        for(int i$ = 0; i$ < len$; ++i$) {
            String file = arr$[i$];
            dir.copy(this, file, file, context);//把索引copy一份給本實例變量,也就是RAMDirectory,從而實現內存目錄索引
        }

        if(closeDir) {
            dir.close();//然后?把Directory給關了,以后用內存目錄索引。
        }
    }
//列出內存中所有的文件信息
    public final String[] listAll() {
        this.ensureOpen();
        Set<String> fileNames = this.fileMap.keySet();
        List<String> names = new ArrayList(fileNames.size());
        Iterator i$ = fileNames.iterator();

        while(i$.hasNext()) {
            String name = (String)i$.next();
            names.add(name);
        }

        return (String[])names.toArray(new String[names.size()]);
    }
//判斷內存目錄中是否存在我們想查的filename
    public final boolean fileExists(String name) {
        this.ensureOpen();
        return this.fileMap.containsKey(name);
    }
//其實操作的是內存上的File對象,也就是RAMFile 
    public final long fileLength(String name) throws IOException {
        this.ensureOpen();
        RAMFile file = (RAMFile)this.fileMap.get(name);
        if(file == null) {
            throw new FileNotFoundException(name);
        } else {
            return file.getLength();
        }
    }

    public final long sizeInBytes() {
        this.ensureOpen();
        return this.sizeInBytes.get();
    }
// 從當前集合中刪除名為name的文件對象
    public void deleteFile(String name) throws IOException {
        this.ensureOpen();
        RAMFile file = (RAMFile)this.fileMap.remove(name);
        if(file != null) {
            file.directory = null;
            this.sizeInBytes.addAndGet(-file.sizeInBytes);
        } else {
            throw new FileNotFoundException(name);
        }
    }
  /**
   * 創建一個新的文件RAMFile 
   * 如果Directory中已經存在一個當前Name的File,
   * 則刪除現有的這個File,將新的File加入到Directory中來.

    這個函數是創建一個名稱為name的輸出流。這里牽扯到一個RAMFile對象和RAMOutputStream對象。RAMFile對象就是在內存中維護一個當前file信息的對象.
    RAMFile ---內存中組織的一個File對象 ,實際上是一個byte[]的數組鏈表。
   */
    public IndexOutput createOutput(String name, IOContext context) throws IOException {
        this.ensureOpen();
        RAMFile file = this.newRAMFile();
        RAMFile existing = (RAMFile)this.fileMap.remove(name);
        /**
       * 加入一個File對象已經存在,需要將原有的那個從集合中排除掉
       * 但是它所對應的相關信息沒有消失
       * 由于沒有其它對象引用這個排除掉的File對象,因此它很快會被GC回收掉
       */
        if(existing != null) {
            this.sizeInBytes.addAndGet(-existing.sizeInBytes);
            //這個地方需要將existing中引用的directory對象置為空
            existing.directory = null;
        }

        this.fileMap.put(name, file);
        return new RAMOutputStream(file);
    }

    protected RAMFile newRAMFile() {
        return new RAMFile(this);
    }

    public void sync(Collection<String> names) throws IOException {
    }
//打開一個input流對象
    public IndexInput openInput(String name, IOContext context) throws IOException {
        this.ensureOpen();
        RAMFile file = (RAMFile)this.fileMap.get(name);
        if(file == null) {
            throw new FileNotFoundException(name);
        } else {
            return new RAMInputStream(name, file);
        }
    }
//關閉內存目錄操作
    public void close() {
        this.isOpen = false;
        this.fileMap.clear();
    }
}

在并發狀態下,管理鎖資源的關鍵點就在SingleInstanceLockFactory 類

/*
    因此,多個線程要進行加鎖操作的時候,需要考慮同步問題。這主要是在獲取SingleInstanceLockFactory中的SingleInstanceLock的時候,同步多個線程,包括請求加鎖、釋放鎖,以及與此相關的共享變量。
*/
public class SingleInstanceLockFactory extends LockFactory {
    private HashSet<String> locks = new HashSet();

    public SingleInstanceLockFactory() {
    }

    public Lock makeLock(String lockName) {
     //從鎖工廠中, 根據指定的鎖lockName返回一個SingleInstanceLock實例
        return new SingleInstanceLock(this.locks, lockName);
    }

    public void clearLock(String lockName) throws IOException {
        HashSet var2 = this.locks;
        synchronized(this.locks) { // 從SingleInstanceLockFactory中清除某個鎖的時候,需要同步
            if(this.locks.contains(lockName)) {
                this.locks.remove(lockName);
            }
        }
    }
}

RAMFile ---內存中組織的一個File對象 ,實際上是一個byte[]的數組鏈表。用這個對象去操作內存中的目錄。

public class RAMFile {
//buffers 保存了File對象中的所有數據信息。RAMOutputStream和RAMInputStream都是對這個buffers對象進行操作。
    protected ArrayList<byte[]> buffers = new ArrayList();
    long length;
    RAMDirectory directory;
    protected long sizeInBytes;

    public RAMFile() {
    }

    RAMFile(RAMDirectory directory) {
        this.directory = directory;
    }
    //得到File長度
    public synchronized long getLength() {
        return this.length;
    }

    protected synchronized void setLength(long length) {
        this.length = length;
    }
    //擴容Buffer
    protected final byte[] addBuffer(int size) {
        byte[] buffer = this.newBuffer(size);
        synchronized(this) {
            this.buffers.add(buffer);
            this.sizeInBytes += (long)size;
        }

        if(this.directory != null) {
            this.directory.sizeInBytes.getAndAdd((long)size);
        }

        return buffer;
    }
//得到這個字節流對象
    protected final synchronized byte[] getBuffer(int index) {
        return (byte[])this.buffers.get(index);
    }

    protected final synchronized int numBuffers() {
        return this.buffers.size();
    }

    protected byte[] newBuffer(int size) {
        return new byte[size];
    }

    public synchronized long getSizeInBytes() {
        return this.sizeInBytes;
    }
}


源碼下載:Lucene總結系列Demo

好了,Lucene總結系列(三)--總述優化方案和呈現實時內存目錄索引講完了,這是項目過程中針對lucene的優化思路,現在一一羅列給大家,并用這一系列的文章結合源碼去深入講解這些優化思路,這是積累的必經一步,我會繼續出這個系列文章,分享經驗給大家。歡迎在下面指出錯誤,共同學習??!你的點贊是對我最好的支持??!

更多內容,可以訪問JackFrost的博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容