Fork me on GitHub

Lucene 源码系列——LRUQueryCache

原文地址: https://www.amazingkoala.com.cn/Lucene/Search/2019/0506/57.html

   LRUQueryCache 用来对一个 Query 查询的结果进行缓存,缓存的内容仅仅是文档号集,由于不会缓存文档的打分(Score),所以只有不需要打分的收集器(Collector)才可以使用 LRUQueryCache,比如说 TotalHitCountCollector 收集器,另外缓存的文档号集使用 BitDocIdSet 对象进行存储,在 BitDocIdSet 中实际使用了 FixedBitSet 对象进行存储。
  即使使用了不需要打分的收集器,也不一定对所有的查询结果进行缓存,有诸多苛刻的条件,在下文中会详细介绍。
  LRUQueryCache 中缓存的 Query 结果是有上限限制的,在每次添加一个缓存时,根据两种阈值来判断是否需要将某个已经缓存的数据剔除,使用的算法为 LRU:

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

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

LRUQueryCache 流程图

图 1:
1.png

开始

图 2:

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

Query

图 3:
3.png

查询定义的 Query 对象。

允许缓存?

图 4:
4.png

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

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:
5.png

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

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

允许增加缓存条件

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:
7.png

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

结语

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


本文地址:https://www.6aiq.com/article/1586695917701
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出