Lucene總結(jié)系列(三)--總述優(yōu)化方案和呈現(xiàn)實(shí)時(shí)內(nèi)存索引實(shí)現(xiàn)(結(jié)合RAMDirectory源碼解析)

前兩篇講清楚基礎(chǔ)和基本api調(diào)用,接下來我們就是要進(jìn)入優(yōu)化篇章了。

本系列:

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

(2)SSM框架構(gòu)建積分系統(tǒng)和基本商品檢索系統(tǒng)(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(2)建立商品數(shù)據(jù)庫和Lucene的搭建

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

(4)Redis系列(二)--緩存設(shè)計(jì)(整表緩存以及排行榜緩存方案實(shí)現(xiàn))

(5) Lucene總結(jié)系列(一)--認(rèn)識(shí)、helloworld以及基本的api操作。

(6)Lucene總結(jié)系列(二)--商品檢索系統(tǒng)的文字檢索業(yè)務(wù)(lucene項(xiàng)目使用)


文章結(jié)構(gòu):(1)總述優(yōu)化方案;(2)呈現(xiàn)實(shí)時(shí)內(nèi)存目錄索引(結(jié)合源碼解析);


一、總述優(yōu)化方案:

(1)索引創(chuàng)建優(yōu)化:

1. 先將索引寫入RAMDirectory,再批量寫 入FSDirectory,不管怎樣,目的都是盡量少的文件IO,因?yàn)閯?chuàng)建索引的最大瓶頸在于磁盤IO。

2. 通過設(shè)置IndexWriter的參數(shù)優(yōu)化索引建立

IndexWriter的forceMerge方法。當(dāng)小文件達(dá)到多少個(gè)時(shí),就自動(dòng)合并多個(gè)小文件為一個(gè)大文件,因?yàn)樗氖褂么鷥r(jià)較高不意見使用此方法,默認(rèn)情況下lucene會(huì)自己合并。合并cfs文件。比如設(shè)定10,就是當(dāng)小文件達(dá)到10個(gè)就自動(dòng)合并成一個(gè)索引cfs文件。(而且只能在close前一步使用)

打開 IndexWriter 的時(shí)候,設(shè)置 autoCommit = false同傳統(tǒng)的數(shù)據(jù)庫操作一樣,批量提交事務(wù)性能總是比每個(gè)操作一個(gè)事務(wù)的性能能好很多。

3.在建立索引過程中,使用單例的 IndexWriter基于內(nèi)存執(zhí)行 Flush 而不是基于 document count--也就是內(nèi)存消耗flush代替文檔數(shù)量flush。indexWriter可以自動(dòng)根據(jù)內(nèi)存消耗調(diào)用flush()??梢允褂胕ndexWriterConfig.setRAMBufferSizeMB(double)設(shè)置緩沖區(qū)大小。測試表明48MB為叫合適值。

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

5.創(chuàng)建單例的IndexWriter。

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

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

(2)搜索索引時(shí)優(yōu)化:

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

通過RAMDirectory內(nèi)存讀寫緩寫提高性能

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

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

[二]RangeFilter替代。用RangeFilter代替RangeQuery,經(jīng)測試速度不會(huì)比RangeQuery慢,但是仍然有性能瓶頸,查詢的90%以上時(shí)間耗費(fèi)在 RangeFilter,研究其源碼發(fā)現(xiàn)RangeFilter實(shí)際上是首先遍歷所有索引,生成一個(gè)BitSet,標(biāo)記每個(gè)document,在時(shí)間范圍內(nèi)的標(biāo)記為true,不在的標(biāo)記為false,然后將結(jié)果傳遞給Searcher查找,這是十分耗時(shí)的。

針對Filter再進(jìn)一步的優(yōu)化:

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

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

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

所以:

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

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

3.IndexSearcher單例化。

(3)其余零散的優(yōu)化點(diǎn):

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

2. 使用更快的分析器。主要是對磁盤空間的優(yōu)化,可以將索引文件減小將近一半,相同測試數(shù)據(jù)下由600M減少到380M。但是對時(shí)間并沒有什么幫助,甚至?xí)枰L時(shí) 間,因?yàn)檩^好的分析器需要匹配詞庫,會(huì)消耗更多cpu

3. 關(guān)鍵詞區(qū)分大小寫。or AND TO等關(guān)鍵詞是區(qū)分大小寫的,lucene只認(rèn)大寫的,小寫的當(dāng)做普通單詞。

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

部分參考

方案大致列舉這些,然后后面的文章會(huì)以這個(gè)為根結(jié)點(diǎn)不斷去擴(kuò)散的,并且結(jié)合源碼解讀下進(jìn)一步的優(yōu)化方案。


二、呈現(xiàn)實(shí)時(shí)內(nèi)存索引(結(jié)合源碼解析):

(1)代碼實(shí)現(xiàn)以及優(yōu)化效果展示:

第一次索引平均時(shí)間(無內(nèi)存索引)

這里寫圖片描述

第一次索引平均時(shí)間(無內(nèi)存索引)

這里寫圖片描述

第一次索引平均時(shí)間(建立內(nèi)存索引)。

這里寫圖片描述

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

這里寫圖片描述
//查看我們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);
            // 創(chuàng)建內(nèi)存索引庫,讓硬盤的庫交給內(nèi)存庫
            ramDirectory = new RAMDirectory(directory_sp, null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

  public static IndexSearcher getIndexSearcherOfSP() throws IOException {

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

        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        return indexSearcher;
    }

(2)源碼分析:

RAMDirectory類是一個(gè)駐留內(nèi)存的(memory-resident)Directory抽象類的實(shí)現(xiàn)。

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

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

    }
/*
  * 僅僅當(dāng)硬盤中的索引能全部放入內(nèi)存中的時(shí)候才能調(diào)用此方法,它會(huì)將所有的現(xiàn)有Index放入到內(nèi)存中來。。也就是索引比較小的時(shí)候才用的方案。大概小于1G。否則得話將會(huì)發(fā)生OOM的異常。
  * 通過這種方法得到的RAMDirectory對象是一個(gè)獨(dú)立于以前的directory對象的新的索引對象
  * 對于以前的Directory對象的任何修改都不會(huì)對新的RAMDirectory對象造成影響
  * 因?yàn)樾碌膶ο笾邪械囊延衖ndex的文件信息
*/
    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一份給本實(shí)例變量,也就是RAMDirectory,從而實(shí)現(xiàn)內(nèi)存目錄索引
        }

        if(closeDir) {
            dir.close();//然后?把Directory給關(guān)了,以后用內(nèi)存目錄索引。
        }
    }
//列出內(nèi)存中所有的文件信息
    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()]);
    }
//判斷內(nèi)存目錄中是否存在我們想查的filename
    public final boolean fileExists(String name) {
        this.ensureOpen();
        return this.fileMap.containsKey(name);
    }
//其實(shí)操作的是內(nèi)存上的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();
    }
// 從當(dāng)前集合中刪除名為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);
        }
    }
  /**
   * 創(chuàng)建一個(gè)新的文件RAMFile 
   * 如果Directory中已經(jīng)存在一個(gè)當(dāng)前Name的File,
   * 則刪除現(xiàn)有的這個(gè)File,將新的File加入到Directory中來.

    這個(gè)函數(shù)是創(chuàng)建一個(gè)名稱為name的輸出流。這里牽扯到一個(gè)RAMFile對象和RAMOutputStream對象。RAMFile對象就是在內(nèi)存中維護(hù)一個(gè)當(dāng)前file信息的對象.
    RAMFile ---內(nèi)存中組織的一個(gè)File對象 ,實(shí)際上是一個(gè)byte[]的數(shù)組鏈表。
   */
    public IndexOutput createOutput(String name, IOContext context) throws IOException {
        this.ensureOpen();
        RAMFile file = this.newRAMFile();
        RAMFile existing = (RAMFile)this.fileMap.remove(name);
        /**
       * 加入一個(gè)File對象已經(jīng)存在,需要將原有的那個(gè)從集合中排除掉
       * 但是它所對應(yīng)的相關(guān)信息沒有消失
       * 由于沒有其它對象引用這個(gè)排除掉的File對象,因此它很快會(huì)被GC回收掉
       */
        if(existing != null) {
            this.sizeInBytes.addAndGet(-existing.sizeInBytes);
            //這個(gè)地方需要將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 {
    }
//打開一個(gè)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);
        }
    }
//關(guān)閉內(nèi)存目錄操作
    public void close() {
        this.isOpen = false;
        this.fileMap.clear();
    }
}

在并發(fā)狀態(tài)下,管理鎖資源的關(guān)鍵點(diǎn)就在SingleInstanceLockFactory 類

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

    public SingleInstanceLockFactory() {
    }

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

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

RAMFile ---內(nèi)存中組織的一個(gè)File對象 ,實(shí)際上是一個(gè)byte[]的數(shù)組鏈表。用這個(gè)對象去操作內(nèi)存中的目錄。

public class RAMFile {
//buffers 保存了File對象中的所有數(shù)據(jù)信息。RAMOutputStream和RAMInputStream都是對這個(gè)buffers對象進(jìn)行操作。
    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;
    }
    //擴(kuò)容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;
    }
//得到這個(gè)字節(jié)流對象
    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總結(jié)系列Demo

好了,Lucene總結(jié)系列(三)--總述優(yōu)化方案和呈現(xiàn)實(shí)時(shí)內(nèi)存目錄索引講完了,這是項(xiàng)目過程中針對lucene的優(yōu)化思路,現(xiàn)在一一羅列給大家,并用這一系列的文章結(jié)合源碼去深入講解這些優(yōu)化思路,這是積累的必經(jīng)一步,我會(huì)繼續(xù)出這個(gè)系列文章,分享經(jīng)驗(yàn)給大家。歡迎在下面指出錯(cuò)誤,共同學(xué)習(xí)!!你的點(diǎn)贊是對我最好的支持?。?/h3>

更多內(nèi)容,可以訪問JackFrost的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容