前兩篇講清楚基礎(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;
}
}