缓存对于提高搜索引擎的吞吐量,降低CPU占用率极为重要。Lucene/Solr在这块做了很多的工作。Lucene/Solr中默认提供了5种缓存,同时solr还提供扩展缓存接口,允许开发者自定义缓存。

1 缓存的基本原理

Solr实现了两种策略的缓存:LRU(Leatest Recently Used)和LFU(Least Frequently Used)。这两种策略也用于操作系统的内存管理(页面置换)。当然缓存还有其它的策略,比如FIFO、Rand等。无论是基于什么样的策略,在应用中命中率高且实现简单的策略才是好策略。

1.1 LRU策略

LRU,又称最近最少使用。假如缓存的容量为10,那么把缓存中的对象按访问(插入)的时间先后排序,当容量不足时,***时间最早的。(当然,真正的实现是通过链表维护时间先后顺序)

1.1.1 LRUCache

Solr中LRUCache是通过LinkedHashMap来实现的。通过LRUCache的init方法就可以发现,其代码如下:

  map = new LinkedHashMap<K,V>(initialSize, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {if (size() > limit) {// increment evictions regardless of state.// this doesn't need to be synchronized because it will// only be called in the context of a higher level synchronized block.evictions++;stats.evictions.incrementAndGet();return true;}return false;}};

需要注意的是其构造参数的最后一个accessOrder。这里accessOrder=true,表明map.get()方法会改变链表的结构,如果accessOrder为false,则map.get()方法不对改变LinkedHashMap中链表的结构,就无法体现最近最小使用这个特点了。

由于LRUCache其本质是LinkedHashMap,而HashMap不是线程安全的,所以就需要在get和put时进行同步,锁住整个map,所以在高并发条件下,其性能会有所影响。因此Solr用另外一种方式实现了LRUCache,即FastLRUCache。

1.1.2 FastLRUCache

FastLRUCache内部采用了ConcurrentLRUCache实现,而ConcurrentLRUCache内部又采用ConcurrentHashMap实现,所以是线程安全的。缓存通过CacheEntry中的访问标记lastAccessed来维护CacheEntry被访问的先后顺序。 即每当Cache有get或者put操作,则当前CacheEntry的lastAccessed都会变成最大的(state.accessCounter)。当FastLRUCache容量已满时,通过markAndSweep方式来剔除缓存中lastAccessed最小的N个项以保证缓存的大小达到一个acceptable的值。

markAndSweep分两个阶段执行:第一阶段收回最近最少使用的项;如果经过第一阶段缓存的大小依然大于acceptable,那么第二阶段将会开始。第二阶段会更加严格地把缓存的大小降下来。

在第一阶段,一个数轴就可以把运行原理解释清楚。

对应代码如下(见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);}}}

看代码可知,第一阶段会按相同的逻辑运行两次。一般来说,经过第一阶段,缓存的大小应该控制下来了。如果依然控制不下来,那么就把上图中的待定Entry直接扔到指定大小的优先队列中。最后把优先队列中的Entry全部***。这样,就能够保证缓存的Size降下来。其实如果一开始就直接上优先队列,代码会少很多。但是程序的性能会降低好多。

通过分析可以看到,如果缓存中put操作频繁,很容易触发markAndSweep方法的执行。而markAndSweep操作比较耗时。所以这部分的操作可以通过设置newThreadForCleanup=true来优化。即新开一个线程执行。这样就不会阻塞put方法。在solrconfig.xml中配置,是这样的cleanupThread=true。Cache在构造的时候就会开启一个线程。通过线程的wait/nofity来控制markAndSweep。从而避免了newThreadForCleanup=true这样的不停开线程的开销,总而言之,缓存是通过markAndSweep来控制容量。

1.2 LFU策略

LFU策略即【最近最少使用】策略。当缓存已满时,设定时间段内使用次数最少的缓存将被剔除出去。通过前面的描述,容易看出LFU策略实现时,必须有一个计数器来记录Cache的Entry被访问的次数。Solr也正是这么干的。(看CacheEntry结构)

 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 来记录访问次数。lastAccessed 存在则是为了应付控制缓存容量时,如果在待***队列中出现hits相同的CacheEntry,那么***lastAccessed 较小的一个。hitsCopy 和lastAccessedCopy的存在则是基于性能的考虑。避免多线程时内存跨越内存栅栏。

LFUCache通过ConcurrentLFUCache来实现,而ConcurrentLFUCache内部又是ConcurrentHashMap。我们关注的重点放在ConcurrentLFUCache。

ConcurrentLFUCache对容量的控制依然是markAndSweep,我猜想这是为了在代码可读性上与ConcurrentLRUCache保持一致。

相对ConcurrentLRUCache的markAndSweep实现而言,ConcurrentLFUCache的markAndSweep就比较简单了。用一个TreeSet来维护待***队列。TreeSet排序则是基于hits 和lastAccessed 。(可参看CacheEntry的comparTo方法)

markAndSweep方法的核心代码如下:

TreeSet<CacheEntry> tree = new TreeSet<CacheEntry>();for (CacheEntry<K, V> ce : map.values()) {// set hitsCopy to avoid later Atomic readsce.hitsCopy = ce.hits.get();ce.lastAccessedCopy = ce.lastAccessed;if (timeDecay) {ce.hits.set(ce.hitsCopy >>> 1);}if (tree.size() < wantToRemove) {tree.add(ce);} else {// If the hits are not equal, we can remove before adding// which is slightly fasterif (ce.hitsCopy < tree.first().hitsCopy) {tree.remove(tree.first());tree.add(ce);} else if (ce.hitsCopy == tree.first().hitsCopy) {tree.add(ce);tree.remove(tree.first());}}}for (CacheEntry<K, V> e : tree) {evictEntry(e.key);}

Solr实现了LFUCache,却没有再来一个FastLFUCache。因为LFUCache的实现用的是ConcurrentHashMap。能够很好的支持并发。如果非要来一个FastLFUCache,那么就得用上非阻塞数据结构了。

2 缓存在Solr的中应用

前面已经提到过,Solr实现了各种层次的缓存。缓存由SolrIndexSearcher集中控制。分别应用在query、fact等查询相关的操作上。

2.1 filterCache

filterCache在SolrIndexSearcher的定义如下:

SolrCache<Query,DocSet> filterCache;

filterCache的key是Query,value是DocSet对象。而DocSet的基本功能就是过滤。filter在英语中的解释是"过滤器"。那么哪些地方有可能用到过滤功能呢?

filterCache在solr中的应用包含以下场景:

1、查询参数facet.method=enum

2、如果solrconfig.xml中配置<useFilterForSortedQuery/> 为true

3、查询参数含Facet.query或者group.query

4、查询参数含fq

2.2 fieldvalueCache

fieldValueCache在SolrIdexSearcher的定义如下:

SolrCache<String,UnInvertedField> fieldValueCache;

其中key代表FieldName,value是一种数据结构UnInvertedField。

fieldValueCache在solr中只用于multivalued Field。一般用到它的就是facet操作。关于这个缓存需要注意的是,如果没有在solrconfig.xml中配置,那么它是默认存在的(初始大小10,最大10000,不会autowarm) 会有内存溢出的隐患。

由于该cache的key为FieldName,而一般一个solrCore中的字段最多也不过几百。在这么多字段中,multivalued 字段会更少,会用到facet操作的则少之又少。所以该在solrconfig.xml中的配置不必过大,大了也是浪费。

该缓存存储排序好的docIds,一般是topN。这个缓存占用内存会比filterCache 小。因为它存储的是topN。但是如果QueryCommand中带有filter(DocSet类型),那么该缓存不会起作用。原因是:DocSet在执行hashcode和equals方法时比较耗时。

2.4 documentCache

该缓存映射docId->Document。没有什么值得多说的。

2.5 自定义缓存

如果solr中实现的缓存不满足需求。那么可以在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执行warm方法时会被调用。假如solr的索引2分钟更新一次,为了保证更新的索引能够被搜索到,那么就需要重新打开一个SolrIndexSearcher,这时候就有一个问题:SolrIndexSearcher里面的缓存怎么办?

如果把旧的缓存全部抛弃,那么搜索的性能势必下降。Solr的做法是通过warm方法来预热缓存。即把通过原有缓存里面的Key值,重新获取一次value。warm完毕后再切换到新的Searcher。regenrator里面的regenerateItem方法就是用来更新缓存。关注一下regenerateItem的参数:

public boolean regenerateItem(SolrIndexSearcher newSearcher, SolrCache newCache, SolrCache oldCache, Object oldKey, Object oldVal) throws IOException;

有SolrIndexSearcher,有oldCache,有oldKey,有oldVal想查询结果很容易就能得到了。这样做的话已经***到Solr内部了,不推荐。如果以后想要升级的话,可能得重新改代码。升级维护不太方便。

2.6 fieldCache

我们知道lucene保存了正向索引(docId-->field)和反向索引(field-->docId)。反向索引是搜索的核心,检索速度很快。但是如果我们需要快速由docId得到Field信息(比如按照某个字段排序,字段值的信息统计<solr facet功能>),由于需要磁盘读取,速度会比较慢。因此Lucene实现了fieldCache。

Lucene实现了各种类型Field的缓存:Byte,Short,Int,Float,Long……

fieldCache是Lucene内部的缓存,主要用于缓存Lucene搜索结果排序,比如按时间排序等。由于fieldCache内部利用数组来存储数据(可以参看FieldCacheImpl源码),而且数组的大小开的都是maxDoc,所以当数据量较大时,fieldCache是相当消耗内存的,所以很容易出现内存溢出问题。

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();}
}

转载于:https://blog.51cto.com/sbp810050504/1421546

理解Lucene/Solr的缓存相关推荐

  1. 全文搜索技术 Lucene solr es (二)Solr(7.7.1)

    学习视频地址:https://www.bilibili.com/video/av45567492?from=search&seid=14848044148453483902 本篇博客是基于此学 ...

  2. Lucene / Solr 开发经验

    Lucene / Solr 开发经验 http://clayz.iteye.com/blog/240357 2008-09-10 Lucene / Solr 开发经验 博客分类:Framework S ...

  3. 理解DataSet的数据缓存机制

    虽然在以前的开发中经常使用DataSet类,但是重来没有涉及到数据缓存机制这块内容.今天看了一下,也算是做点总结. 在理解数据缓存机制之前需要理解DataRow的两个概念,即行状态和行版本.行状态就是 ...

  4. 理解分布式系统中的缓存架构(下)

    承接上一篇<理解分布式系统中的缓存架构(上)>,介绍了大型分布式系统中缓存的相关理论,常见的缓存组件以及应用场景,本文主要介绍缓存架构设计常见问题以及解决方案,业界案例. 1. 分层缓存架 ...

  5. 理解分布式系统中的缓存架构(上)

    本文主要介绍大型分布式系统中缓存的相关理论,常见的缓存组件以及应用场景. 1. 缓存概述 缓存概述 2. 缓存的分类 缓存主要分为以下四类 缓存的分类 2.1 CDN缓存 基本介绍 CDN(Conte ...

  6. 深入理解分布式系统中的缓存架构(下)

    转载自   深入理解分布式系统中的缓存架构(下) 承接上一篇<理解分布式系统中的缓存架构(上)>,介绍了大型分布式系统中缓存的相关理论,常见的缓存组件以及应用场景,本文主要介绍缓存架构设计 ...

  7. 深入理解分布式系统中的缓存架构(上)

    转载自   深入理解分布式系统中的缓存架构(上) 本文主要介绍大型分布式系统中缓存的相关理论,常见的缓存组件以及应用场景. 1 缓存概述 2 缓存的分类 缓存主要分为以下四类 2.1 CDN缓存 基本 ...

  8. 理解内存和文件系统缓存

    理解记忆和文件系统缓存 2出4额定的帮助- 率这一主题 窗户 2000分配部分的虚拟内存系统的文件系统缓存.文件系统缓存内存子系统,保留最近使用的信息快速存取.缓存的大小取决于物理内存的安装和记忆所需 ...

  9. 理解Lucene索引与搜索过程中的核心类

    理解索引过程中的核心类 执行简单索引的时候需要用的类有: IndexWriter.Directory.Analyzer.Document.Field 1.IndexWriter IndexWriter ...

最新文章

  1. 毕业设计(3)基于MicroPython的篮球计时计分器模型的设计与实现
  2. 北理计算机考研机试,北理工计算机2000-2010考研机试题目c语言实现.doc
  3. 阿里云 SSL 证书 总结
  4. CentOS 6.3安装(详细图解教程)
  5. Mybatis 系列2-配置文件
  6. 11. 搭建一个完整的K8S集群
  7. java 随机生成大写字母_java 生成随机大写字母,整数,小写字母
  8. Linux下的压缩和解压缩命令——compress/uncompress
  9. 8002雨过天晴等冠号
  10. SuperMap云许可配置
  11. 计算机教室冷量负荷,7.2空调负荷计算 - 民用建筑供暖通风与空气调节设计规范 GB50736-2012 - 消防规范大全 - 消防资源网!...
  12. Codeforces-Round#548(Div.2)-C-Edgy Trees-快速幂
  13. 每周工作总结-记录总结自己遇到问题及学习内容,及时分析,找到不足,让自己不断进步
  14. Tiny Core Linux 安装配置
  15. 关于JeecgBoot 的项目理解、使用心得和改进建议
  16. TiKV源码略读-Server Start
  17. JNDI--Java命名与目录接口
  18. Arduino制作温湿度计
  19. Linux下通过ODBC连接mysql orical sqlServer数据库
  20. Python 特殊字符处理

热门文章

  1. linux与windows编码转化
  2. C语言 有符号字符型输出 面试题
  3. appium 设置参数
  4. Linux下没有包含头文件(不知是哪个)导致编译无法通过的解决心得
  5. java接口作用和好处,持续更新大厂面试笔试题
  6. mysql数据库开发的36条军规
  7. 【嵌入式硬件Esp32】Ubuntu 1804下ESP32交叉编译环境搭建
  8. Codeforces 626F Group Projects (DP)
  9. Leetcode: Kth Largest Element in an Array
  10. mysql 组合索引