LRUQueryCache

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

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

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

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

LRUQueryCache流程图

图1:

开始

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

Query

图3: 查询定义的Query对象。

允许缓存?

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

Query条件

有些Query不需要缓存:

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

索引文件条件

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

段(Segment)条件

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

子IndexReader条件

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

线程竞争条件

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

不存在缓存?

当满足缓存条件后,继续下面的流程 图5: 如果存在缓存,那么直接取出缓存就可以退出了,需要重复的是,返回的结果只是文档号集。 图6: 如果不存在缓存,那么我们需要增加缓存,但是增加缓存还存在一些额外条件:

允许增加缓存条件

Query条件

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

历史查询条件

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

执行查询,保存结果

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

执行LRU?

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

结语

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

点击下载Markdown文件