LRUQueryCache
Lu Xugang Lv6

   LRUQueryCache用来对一个Query查询的结果进行缓存,缓存的内容仅仅是文档号集,由于不会缓存文档的打分(Score),所以只有不需要打分的收集器(Collector)才可以使用LRUQueryCache,比如说TotalHitCountCollector收集器,另外缓存的文档号集使用BitDocIdSet对象进行存储,在BitDocIdSet中实际使用了FixedBitSet对象进行存储。

  即使使用了不需要打分的收集器,也不一定对所有的查询结果进行缓存,有诸多苛刻的条件,在下文中会详细介绍。

  LRUQueryCache中缓存的Query结果是有上限限制的,在每次添加一个缓存时,根据两种阈值来判断是否需要将某个已经缓存的数据剔除,使用的算法为LRU:

  • maxCachedQueries:缓存的结果数量,默认值1000
  • maxRamBytesUsed:缓存占用的内存量,默认值32MB或可分配内存的5%中的较小值

当然,我们可以自定义maxCachedQueries跟maxRamBytesUsed的值。

LRUQueryCache流程图

图1:

开始

图2:

在执行查询时,如果我们使用了一个不需要打分的Collector,那么该Query就可以进入LRUQueryCache的流程之中,比如说TermsCollector、TotalHitCountCollector等等。默认的查询使用的是TopScoreDocCollector,他需要打分,所以无法使用LRUQueryCache的功能。

Query

图3:

查询定义的Query对象。

允许缓存?

图4:

允许缓存受限于诸多条件,下面一一列出:

Query条件

有些Query不需要缓存:

  • TermQuery:TermQuery作为最常用的Query,源码中给出不需要缓存的理由是这种查询已经足够快(plenty fast)了。
  • MatchAllDocsQuery:此Query是获得IndexReader中的所有文档号,在不使用cache的情况下获取所有结果的逻辑是从0开始遍历到IndexReader中的最大的文档号(因为每一篇文档都是满足要求的),如果缓存这个结果的话,由于使用FixedBitSet存储了cache,在生成缓存的过程中需要编码,并且读取cache还需要解码,查询性能肯定是相比较大的。
  • MatchNoDocsQuery:此Query不会匹配任何文档,所以没有缓存的必要
  • BooleanQuery:BooleanQuery中没有任何子Query是不用缓存的
  • DisjunctionMaxQuery:DisjunctionMaxQuery中没有任何子Query是不用缓存的

满足了Query条件后,会将当前Query(Query对象的HashCode)添加到LRU算法中,并且当前Query为cache中最近最新使用,为了后面执行LRU算法做准备。

索引文件条件

根据当前的索引文件条件决定是否允许缓存,比如说存放DocValues.dvm、.dvd文件在更新后,那么就不允许缓存。

段(Segment)条件

即使满足Query条件、索引文件条件,还要考虑当前段中的条件,条件跟当前段中包含的文档数量相关:

  • 最坏的情况下缓存所有子IndexReader中所有的文档号需要的内存量(worstCaseRamUsage)、目前最大可使用的内存量(totalRamAvailable:),这个值即上文中的maxRamBytesUsed,当满足(worstCaseRamUsage * 5) < totalRamAvailable时就允许缓存
  • 当前子IndexReader中包含的文档号数量 perReaderDocNum ≥ minSize(默认值10000),并且 perReaderDocNum占所有子IndexReader的文档总量totalDocNum满足 (perReaderDocNum / totalDocNum ≥ minSizeRatio (默认值0.03)), 其中minSize跟minSizeRatio可配

子IndexReader条件

不是所有的IndexReader都适合缓存,比如说facet中读取taxonomy的OrdinalMappingLeafReader,在以后的文章中介绍facet会给出原因。

线程竞争条件

当多个线程使用同一个IndexSearcher对象,那么cache就会成为临界区,当前线程如果访问cache发现已被其他线程占用,源码中的处理方式是不等待锁资源,即不使用LRUQueryCache,原因是在高并发下,查询被阻塞的时间可能跟查询个数成正比,反而降低了查询性能。锁资源被占用的情况有以下几种:

  • 其他线程正在添加一个cache,并且这个cache有可能不是当前线程的Query的cache
  • 其他线程正在读取一个cache,源码中使用的不是读写锁,所以即使当前线程可能也是读操作,也无法访问cache

不存在缓存?

当满足缓存条件后,继续下面的流程
图5:

如果存在缓存,那么直接取出缓存就可以退出了,需要重复的是,返回的结果只是文档号集。
图6:

如果不存在缓存,那么我们需要增加缓存,但是增加缓存还存在一些额外条件:

允许增加缓存条件

Query条件

这里的Query条件跟上文中的Query条件是一样的,这里还要继续检查一遍当前的Query是否需要缓存,因为如果某个Query使用多线程在多个子IndexReader中并行查询,由于这些线程使用同一个Weight对象,并且在上文中的Query条件检查中会将当前Query添加到LRU算法中,为了避免重复添加造成错误的计数(相同Query的历史查询计数,下文会介绍),所以在上文中的Query条件除了第一个线程,其他线程会跳过这一步骤,故在这里需要检查Query条件。

历史查询条件

在满足Query条件的前提下,并且同时满足历史查询计数打到阈值,才允许增加缓存,不同的Query对象的阈值是不同的,目前Lucene 7.5.0版本中域值根据Query对象有以下几种数值:

  • 2:如果是MultiTermQuery、MultiTermQueryConstantScoreWrapper、TermInSetQuery、PointQuery(点数据类型的所有查询),那么这些Query在历史查询中出现过2次,就允许增加缓存
  • 4:如果是BooleanQuery、DisjunctionMaxQuery,那么这些Query在历史查询中出现过4次,就允许增加缓存
  • 5:其他类型的Query

执行查询,保存结果

当满足允许增加缓存条件后,就可以执行一次常规的查询,获得查询结果后,即文档号集,存放到FixedBitSet,即缓存。

执行LRU?

图7:

上文中提到,缓存的个数跟占用内存量是有上限限制的,每当添加一个缓存后,会判断是否需要执行LRU算法来剔除某个旧的缓存或者直接添加新的缓存。在随后的文章中会详细介绍在Lucene中LRU算法的实现,因为这不是LRUQueryCache专有的功能,它属于一个Util类,出于正确分类目的,会另外写一篇文章。

结语

文章中一些细节并没有详细介绍,比如说 为什么有些IndexReader不允许缓存、哪些IndexReader不允许缓存、为什么段文件更新后不允许缓存,在后面的文章中会解释这些问题。

点击下载Markdown文件

 Comments