理解Lucene/Solr的缓存
????緩存對(duì)于提高搜索引擎的吞吐量,降低CPU占用率極為重要。Lucene/Solr在這塊做了很多的工作。Lucene/Solr中默認(rèn)提供了5種緩存,同時(shí)solr還提供擴(kuò)展緩存接口,允許開發(fā)者自定義緩存。
1?緩存的基本原理
Solr實(shí)現(xiàn)了兩種策略的緩存:LRU(Leatest?Recently?Used)和LFU(Least?Frequently?Used)。這兩種策略也用于操作系統(tǒng)的內(nèi)存管理(頁面置換)。當(dāng)然緩存還有其它的策略,比如FIFO、Rand等。無論是基于什么樣的策略,在應(yīng)用中命中率高且實(shí)現(xiàn)簡(jiǎn)單的策略才是好策略。
1.1?LRU策略
LRU,又稱最近最少使用。假如緩存的容量為10,那么把緩存中的對(duì)象按訪問(插入)的時(shí)間先后排序,當(dāng)容量不足時(shí),***時(shí)間最早的。(當(dāng)然,真正的實(shí)現(xiàn)是通過鏈表維護(hù)時(shí)間先后順序)
1.1.1?LRUCache
Solr中LRUCache是通過LinkedHashMap來實(shí)現(xiàn)的。通過LRUCache的init方法就可以發(fā)現(xiàn),其代碼如下:
需要注意的是其構(gòu)造參數(shù)的最后一個(gè)accessOrder。這里accessOrder=true,表明map.get()方法會(huì)改變鏈表的結(jié)構(gòu),如果accessOrder為false,則map.get()方法不對(duì)改變LinkedHashMap中鏈表的結(jié)構(gòu),就無法體現(xiàn)最近最小使用這個(gè)特點(diǎn)了。
?
由于LRUCache其本質(zhì)是LinkedHashMap,而HashMap不是線程安全的,所以就需要在get和put時(shí)進(jìn)行同步,鎖住整個(gè)map,所以在高并發(fā)條件下,其性能會(huì)有所影響。因此Solr用另外一種方式實(shí)現(xiàn)了LRUCache,即FastLRUCache。
1.1.2?FastLRUCache
FastLRUCache內(nèi)部采用了ConcurrentLRUCache實(shí)現(xiàn),而ConcurrentLRUCache內(nèi)部又采用ConcurrentHashMap實(shí)現(xiàn),所以是線程安全的。緩存通過CacheEntry中的訪問標(biāo)記lastAccessed來維護(hù)CacheEntry被訪問的先后順序。?即每當(dāng)Cache有get或者put操作,則當(dāng)前CacheEntry的lastAccessed都會(huì)變成最大的(state.accessCounter)。當(dāng)FastLRUCache容量已滿時(shí),通過markAndSweep方式來剔除緩存中lastAccessed最小的N個(gè)項(xiàng)以保證緩存的大小達(dá)到一個(gè)acceptable的值。
markAndSweep分兩個(gè)階段執(zhí)行:第一階段收回最近最少使用的項(xiàng);如果經(jīng)過第一階段緩存的大小依然大于acceptable,那么第二階段將會(huì)開始。第二階段會(huì)更加嚴(yán)格地把緩存的大小降下來。
在第一階段,一個(gè)數(shù)軸就可以把運(yùn)行原理解釋清楚。
對(duì)應(yīng)代碼如下(見ConcurrentLRUCache.markAndSweep方法)
????????
//?since?the?wantToKeep?group?is?likely?to?be?bigger?than?wantToRemove,?check?it?firstif?(thisEntry?>?newestEntry?-?wantToKeep)?{//?this?entry?is?guaranteed?not?to?be?in?the?bottom//?group,?so?do?nothing.numKept++;newOldestEntry?=?Math.min(thisEntry,?newOldestEntry);}?else?if?(thisEntry?<?oldestEntry?+?wantToRemove)?{?//?entry?in?bottom?group?//?this?entry?is?guaranteed?to?be?in?the?bottom?group//?so?immediately?remove?it?from?the?map.evictEntry(ce.key);numRemoved++;}?else?{//?This?entry?*could*?be?in?the?bottom?group.//?Collect?these?entries?to?avoid?another?full?pass...?this?is?wasted//?effort?if?enough?entries?are?normally?removed?in?this?first?pass.//?An?alternate?impl?could?make?a?full?second?pass.if?(eSize?<?eset.length-1)?{eset[eSize++]?=?ce;newNewestEntry?=?Math.max(thisEntry,?newNewestEntry);newOldestEntry?=?Math.min(thisEntry,?newOldestEntry);}}}?????
看代碼可知,第一階段會(huì)按相同的邏輯運(yùn)行兩次。一般來說,經(jīng)過第一階段,緩存的大小應(yīng)該控制下來了。如果依然控制不下來,那么就把上圖中的待定Entry直接扔到指定大小的優(yōu)先隊(duì)列中。最后把優(yōu)先隊(duì)列中的Entry全部***。這樣,就能夠保證緩存的Size降下來。其實(shí)如果一開始就直接上優(yōu)先隊(duì)列,代碼會(huì)少很多。但是程序的性能會(huì)降低好多。
?
通過分析可以看到,如果緩存中put操作頻繁,很容易觸發(fā)markAndSweep方法的執(zhí)行。而markAndSweep操作比較耗時(shí)。所以這部分的操作可以通過設(shè)置newThreadForCleanup=true來優(yōu)化。即新開一個(gè)線程執(zhí)行。這樣就不會(huì)阻塞put方法。在solrconfig.xml中配置,是這樣的cleanupThread=true。Cache在構(gòu)造的時(shí)候就會(huì)開啟一個(gè)線程。通過線程的wait/nofity來控制markAndSweep。從而避免了newThreadForCleanup=true這樣的不停開線程的開銷,總而言之,緩存是通過markAndSweep來控制容量。
?
1.2?LFU策略
LFU策略即【最近最少使用】策略。當(dāng)緩存已滿時(shí),設(shè)定時(shí)間段內(nèi)使用次數(shù)最少的緩存將被剔除出去。通過前面的描述,容易看出LFU策略實(shí)現(xiàn)時(shí),必須有一個(gè)計(jì)數(shù)器來記錄Cache的Entry被訪問的次數(shù)。Solr也正是這么干的。(看CacheEntry結(jié)構(gòu))
?
?private?static?class?CacheEntry<K,?V>?implements?Comparable<CacheEntry<K,?V>>?{K?key;V?value;volatile?AtomicLong?hits?=?new?AtomicLong(0);long?hitsCopy?=?0;volatile?long?lastAccessed?=?0;long?lastAccessedCopy?=?0;public?CacheEntry(K?key,?V?value,?long?lastAccessed)?{this.key?=?key;this.value?=?value;this.lastAccessed?=?lastAccessed;}很清楚地看到CacheEntry用hits?來記錄訪問次數(shù)。lastAccessed?存在則是為了應(yīng)付控制緩存容量時(shí),如果在待***隊(duì)列中出現(xiàn)hits相同的CacheEntry,那么***lastAccessed?較小的一個(gè)。hitsCopy?和lastAccessedCopy的存在則是基于性能的考慮。避免多線程時(shí)內(nèi)存跨越內(nèi)存柵欄。
?
LFUCache通過ConcurrentLFUCache來實(shí)現(xiàn),而ConcurrentLFUCache內(nèi)部又是ConcurrentHashMap。我們關(guān)注的重點(diǎn)放在ConcurrentLFUCache。
ConcurrentLFUCache對(duì)容量的控制依然是markAndSweep,我猜想這是為了在代碼可讀性上與ConcurrentLRUCache保持一致。
相對(duì)ConcurrentLRUCache的markAndSweep實(shí)現(xiàn)而言,ConcurrentLFUCache的markAndSweep就比較簡(jiǎn)單了。用一個(gè)TreeSet來維護(hù)待***隊(duì)列。TreeSet排序則是基于hits?和lastAccessed?。(可參看CacheEntry的comparTo方法)
markAndSweep方法的核心代碼如下:
Solr實(shí)現(xiàn)了LFUCache,卻沒有再來一個(gè)FastLFUCache。因?yàn)?/span>LFUCache的實(shí)現(xiàn)用的是ConcurrentHashMap。能夠很好的支持并發(fā)。如果非要來一個(gè)FastLFUCache,那么就得用上非阻塞數(shù)據(jù)結(jié)構(gòu)了。
?
?
?
?
2?緩存在Solr的中應(yīng)用
前面已經(jīng)提到過,Solr實(shí)現(xiàn)了各種層次的緩存。緩存由SolrIndexSearcher集中控制。分別應(yīng)用在query、fact等查詢相關(guān)的操作上。
2.1?filterCache
filterCache在SolrIndexSearcher的定義如下:
SolrCache<Query,DocSet>?filterCache;
???filterCache的key是Query,value是DocSet對(duì)象。而DocSet的基本功能就是過濾。filter在英語中的解釋是"過濾器"。那么哪些地方有可能用到過濾功能呢?
filterCache在solr中的應(yīng)用包含以下場(chǎng)景:
1、查詢參數(shù)facet.method=enum
2、如果solrconfig.xml中配置<useFilterForSortedQuery/>?為true
3、查詢參數(shù)含Facet.query或者group.query
4、查詢參數(shù)含fq
????
2.2?fieldvalueCache
fieldValueCache在SolrIdexSearcher的定義如下:
SolrCache<String,UnInvertedField>?fieldValueCache;
?
其中key代表FieldName,value是一種數(shù)據(jù)結(jié)構(gòu)UnInvertedField。
?
fieldValueCache在solr中只用于multivalued?Field。一般用到它的就是facet操作。關(guān)于這個(gè)緩存需要注意的是,如果沒有在solrconfig.xml中配置,那么它是默認(rèn)存在的(初始大小10,最大10000,不會(huì)autowarm)?會(huì)有內(nèi)存溢出的隱患。
由于該cache的key為FieldName,而一般一個(gè)solrCore中的字段最多也不過幾百。在這么多字段中,multivalued?字段會(huì)更少,會(huì)用到facet操作的則少之又少。所以該在solrconfig.xml中的配置不必過大,大了也是浪費(fèi)。
該緩存存儲(chǔ)排序好的docIds,一般是topN。這個(gè)緩存占用內(nèi)存會(huì)比filterCache?小。因?yàn)樗鎯?chǔ)的是topN。但是如果QueryCommand中帶有filter(DocSet類型),那么該緩存不會(huì)起作用。原因是:DocSet在執(zhí)行hashcode和equals方法時(shí)比較耗時(shí)。
2.4?documentCache
該緩存映射docId->Document。沒有什么值得多說的。
?
2.5?自定義緩存
如果solr中實(shí)現(xiàn)的緩存不滿足需求。那么可以在SolrConfig.xml中自定義緩存。?
<cache?name="c"class="solr.FastLRUCache"size="4096"initialSize="1024"autowarmCount="1024"regenerator="com.mycompany.cache.CacheRegenerator"/>需要寫代碼的地方就是?regenerator="com.mycompany.cache.CacheRegenerator"這里了。Regenerator在SolrIndexSearcher執(zhí)行warm方法時(shí)會(huì)被調(diào)用。假如solr的索引2分鐘更新一次,為了保證更新的索引能夠被搜索到,那么就需要重新打開一個(gè)SolrIndexSearcher,這時(shí)候就有一個(gè)問題:SolrIndexSearcher里面的緩存怎么辦?
如果把舊的緩存全部拋棄,那么搜索的性能勢(shì)必下降。Solr的做法是通過warm方法來預(yù)熱緩存。即把通過原有緩存里面的Key值,重新獲取一次value。warm完畢后再切換到新的Searcher。regenrator里面的regenerateItem方法就是用來更新緩存。關(guān)注一下regenerateItem的參數(shù):
??public?boolean?regenerateItem(SolrIndexSearcher?newSearcher,?SolrCache?newCache,?SolrCache?oldCache,?Object?oldKey,?Object?oldVal)?throws?IOException;
有SolrIndexSearcher,有oldCache,有oldKey,有oldVal想查詢結(jié)果很容易就能得到了。這樣做的話已經(jīng)***到Solr內(nèi)部了,不推薦。如果以后想要升級(jí)的話,可能得重新改代碼。升級(jí)維護(hù)不太方便。
2.6?fieldCache
我們知道lucene保存了正向索引(docId-->field)和反向索引(field-->docId)。反向索引是搜索的核心,檢索速度很快。但是如果我們需要快速由docId得到Field信息(比如按照某個(gè)字段排序,字段值的信息統(tǒng)計(jì)<solr?facet功能>),由于需要磁盤讀取,速度會(huì)比較慢。因此Lucene實(shí)現(xiàn)了fieldCache。
Lucene實(shí)現(xiàn)了各種類型Field的緩存:Byte,Short,Int,Float,Long……
fieldCache是Lucene內(nèi)部的緩存,主要用于緩存Lucene搜索結(jié)果排序,比如按時(shí)間排序等。由于fieldCache內(nèi)部利用數(shù)組來存儲(chǔ)數(shù)據(jù)(可以參看FieldCacheImpl源碼),而且數(shù)組的大小開的都是maxDoc,所以當(dāng)數(shù)據(jù)量較大時(shí),fieldCache是相當(dāng)消耗內(nèi)存的,所以很容易出現(xiàn)內(nèi)存溢出問題。
?
fieldCache使用的樣例可可參看如下的源代碼。
package?com.vancl.cache;import?java.io.IOException;import?org.apache.lucene.analysis.Analyzer; import?org.apache.lucene.analysis.core.WhitespaceAnalyzer; import?org.apache.lucene.document.Document; import?org.apache.lucene.document.Field.Store; import?org.apache.lucene.document.IntField; import?org.apache.lucene.document.StringField; import?org.apache.lucene.index.DirectoryReader; import?org.apache.lucene.index.IndexReader; import?org.apache.lucene.index.IndexWriter; import?org.apache.lucene.index.IndexWriterConfig; import?org.apache.lucene.search.IndexSearcher; import?org.apache.lucene.search.MatchAllDocsQuery; import?org.apache.lucene.search.ScoreDoc; import?org.apache.lucene.search.Sort; import?org.apache.lucene.search.SortField; import?org.apache.lucene.search.TopDocs; import?org.apache.lucene.search.TopFieldCollector; import?org.apache.lucene.store.Directory; import?org.apache.lucene.store.RAMDirectory; import?org.apache.lucene.util.Version;public?class?TestFieldCache?{Directory?d=?new?RAMDirectory();Analyzer?analyzer?=new?WhitespaceAnalyzer(Version.LUCENE_42);IndexWriterConfig?conf?=?null;IndexWriter?iw?=?null;public?void?index()?throws?IOException{conf?=?new?IndexWriterConfig(Version.LUCENE_42,analyzer);iw?=?new?IndexWriter(d,?conf);Document?doc?=?null;int[]?ids?={1,5,3,2,4,8,6,7,9,10};String[]?addTimes={"2012-12-12?12:12:12","2012-12-12?12:12:13","2012-12-12?12:12:14","2012-12-12?12:12:15","2012-12-12?12:12:11","2012-12-12?12:12:10","2012-12-12?12:12:09","2012-12-12?12:12:08","2012-12-12?12:12:07","2012-12-12?12:12:06"} ;for(int?i=1;i<=10;i++){doc=new?Document();doc.add(new?StringField("addTime",addTimes[i-1],?Store.YES));doc.add(new?IntField("id",ids[i-1],?Store.YES));iw.addDocument(doc);}iw.commit();iw.close();}public?void?query()?throws?IOException{IndexReader?ir?=?DirectoryReader.open(d);IndexSearcher?is?=?new?IndexSearcher(ir);//按addTime逆序排序//Sort?sort?=?new?Sort(new?SortField("addTime",?SortField.Type.STRING,true));Sort?sort?=?new?Sort(new?SortField("addTime",?SortField.Type.STRING,true));//按id逆序排序//Sort?sort?=?new?Sort(new?SortField("id",?SortField.Type.INT,true));TopFieldCollector?collector?= TopFieldCollector.create(sort,?5,?false,?false,?false,?false);is.search(new?MatchAllDocsQuery(),collector);TopDocs?top=?collector.topDocs();for?(ScoreDoc?doc?:?top.scoreDocs)?{// System.out.println(ir.document(doc.doc).get("id"));System.out.println(ir.document(doc.doc).get("addTime"));}}public?static?void?main(String[]?args)?throws?IOException?{TestFieldCache?c?=?new?TestFieldCache();c.index();c.query();} }轉(zhuǎn)載于:https://blog.51cto.com/sbp810050504/1421546
總結(jié)
以上是生活随笔為你收集整理的理解Lucene/Solr的缓存的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 女人梦到玫瑰花预示着什么
- 下一篇: 开源 免费 java CMS - Fre