我们紧接文章ForceMerge(一),继续介绍剩余的内容,先给出强制合并的流程图:
图1:
是否存在可以合并的段?
图2:
当前流程点的内容对应为源码中IndexWriter类的updatePendingMerges(…)方法,如下所示:
图3:
如果用一句话描述该方法的用途,那就是:根据段的合并策略从上一个流程点(收集所有的段
)收集的段集合中筛选出真正参与合并的段集合(段集合由一个或者多个OneMerge(见文章ForceMerge(一)的介绍)组成)。
图3中,这个方法使用synchronized修饰,对应为图1中的第二次synchronized。
另外根据不同的合并触发类型,该方法中会有三种方式来获得段集合。我们先了解下合并的触发类型。
合并触发类型
图4:
SEGMENT_FLUSH
该类型对应的注释在当前版本已经是不准确的描述了。在早期版本中,以Lucene 6.6.0为例,在执行了doFlush( )期间,会调用IndexWriter的doAfterSegmentFlushed( )方法,在这个方法中如果需要进行合并,那么此时的合并触发类型为SEGMENT_FLUSH,而在文章文档提交之flush(一)中我们知道,doFlush( )描述的正是DWPT(见文章文档的增删改(二)的介绍) flush成一个段的过程。
在当前版本中(Lucene 8.8.0),仅仅是在执行了修改索引(见文章文档的增删改(一))的操作后,并且如果产生的删除信息超过了阈值(见文章文档的增删改(五)中flushDeletes、流程点执行flush策略,设置flushDeletes
的介绍),那么在执行完这次修改索引的操作后会触发SEGMENT_FLUSH的合并。
FULL_FLUSH
这种类型的注释是正确的,即当执行了IndexWriter类中的flush()、commit()、getReader()方法以及NRT(NRT中会调用getReader()方法)操作后,会触发合并。
EXPLICIT
用户主动的合并操作属于这种类型。比如IndexWriter类中提供了maybeMerge( )的方法允许用户主动调用合并操作,如图5所示。另外强制合并的操作也属于当前的类型。
图5:
MERGE_FINISHED
由于允许多线程执行段的合并操作,对于某个线程来说,在执行完操作后,会再次尝试执行段的合并操作。例如在段的调度策略ConcurrentMergeScheduler会使用到这种类型:
图6:
在文章段的合并调度MergeScheduler中我们知道,ConcurrentMergeScheduler会开启一个后台线程进行段的合并,即图6的描述。图6中蓝框标注的doMerge(…)方法即执行段的合并。该方法执行结束后,在红框标注的runOnMergeFinished(…)方法中会再次尝试进行段的合并。上述方法在ConcurrentMergeScheduler类中。
CLOSING
在IndexWriter类中,提供了shutdown( )的方法表示当前IndexWriter即将关闭,不再提供索引的增删改等操作。由于段的合并的操作可以是后台线程执行(取决于段的合并调度MergeScheduler),那么Lucene会在IndexWriter关闭前执行这种类型的段的合并操作。如果是CLOSING类型,那么执行段的合并操作的线程将会按照最大的磁盘写入量执行(见文章段的合并调度MergeScheduler中关于最大磁盘写入量的概念)。
COMMIT、GET_READER
这两种合并触发类型在Lucene 8.6.0之后开始依次添加的。在Lucene8.6.0版本之前,我们知道每次执行flush()、commit()、getReader()(比如NRT操作)之后会进行段的合并操作。然而在Lucene8.6.0之后,Lucene开始支持在执行commit()、getReader()期间就进行了段的合并,这些合并的触发类型就是COMMIT或GET_READER。
关于在执行commit()、getReader()期间进行段的合并的内容,感兴趣的同学可以看文章Changes(Lucene 8.7.0)中关于LUCENE-8962的介绍。
流程图
上文中我们说到,图3中的方法,它描述的功能是根据不同的合并触发类型,使用三种方式来获得段集合。其不同方式的选择逻辑如下所示:
图7:
三种方式的选择
图7的流程图中,三种方式的选择对应于源码中的代码块如下所示:
图8:
图8中第2320行代码的findForcedMerges(…) 方法即方式一
,通过该方法将会获得段集合。并且这种方式正是段的强制合并获得段集合的入口方法。对于其他两种方式,方式二以及方式三,本系列文章暂不展开介绍。
注册待合并的段
图9:
当通过三种方式中的一种获取了待合并的段集合(一个或多个OneMerge)后,这些OneMerge会依次进行注册操作。如果注册成功,OneMerge中包含的段将用一个称为mergingSegments的容器存放。该容器的作用可以理解成这些段添加了一个状态,该状态描述的是这些段正在执行段的合并操作。同时还会添加到pendingMerges(见文章ForceMerge(一))容器中。
什么情况下会注册失败
在注册的过程中,会判断每一个段是否已经处于其他线程的合并操作中,即通过mergingSegments是否包含这个段来判断。如果OneMerge中至少一个段已经在mergingSegments中,那么注册将会失败,意味着将不会被添加到pendingMerges容器中,那么就不会进行合并操作。这里需要再次重复下在文章ForceMerge(一)中提到的内容,即pendingMerges是线程共享的容器,执行合并的所有线程总是同步的从这个容器中取出一个OneMerge来执行段的合并操作。
上文的内容同时描述了这么一个事实:多个线程在执行图7中的三个方式中的任意一个后获得了一个或多个OneMerge,这些OneMerge不一定能参与段的合并操作
注册待合并的段还有什么其他作用
由于允许并发执行索引的增删改跟段的合并操作,那么存在这么一种场景,在索引提交阶段,如果某个段中的所有文档都满足删除的条件,这个段会被直接删除。如果此时这个段正在执行段的合并操作,那么就会出现空指针问题。所以在删除某个段前可以通过检查mergingSegments中是否包含此段来判断是否要删除这个段。
- 在文章文档提交之flush(六)介绍流程点
发布FlushedSegment
时我们知道,某些段在索引提交阶段会被丢弃。我们可以看下源码中这个方法来直观的理解:
图10:
如果一个段的文档都满足删除信息(has 100% deleted documents)时,该方法将被调用。红框标注的注释解释了mergingSegments的作用:如果当前段正在合并中,那么把这个段留在readerPool中即可。
注意的是,图10的方法属于IndexWriter类型,并且也用synchronized修饰了。
结语
在下一篇文章中,我们将继续介绍图7中的方式一的实现方式。
点击下载附件