文章目录
Abstract Graph:
流程分析:
1.获取索引目录的索引文件锁
该流程为Lucene使用索引文件锁对索引文件所在的目录进行加锁,使得同一时间总是只有一个IndexWriter对象可以更改索引文件,即保证单进程内(single in-process)多个不同IndexWriter对象互斥更改(多线程持有相同引用的IndexWriter对象视为一个IndexWriter不会受制于LockFactory,而是受制于对象锁(synchronized(IndexWriter))、多进程内(multi-processes)多个对象互斥更改。
想了解索引文件锁更多,移步:餐中餐(7)Lucene 中的文件锁;
public IndexWriter(Directory d, IndexWriterConfig conf) throws IOException {
conf.setIndexWriter(this); // prevent reuse by other instances: 防止被其他的实例重用
config = conf; //它提供了一个半不可变的对象包装器实现,允许一个人只设置一次对象的值,然后多次检索它
infoStream = config.getInfoStream();
softDeletesEnabled = config.getSoftDeletesField() != null;
eventListener = config.getIndexWriterEventListener();
// obtain the write.lock. If the user configured a timeout,
// we wrap with a sleeper and this might take some time.
writeLock = d.obtainLock(WRITE_LOCK_NAME);
......
}
保证写入IndexWriter唯一
############################################################################
private SetOnce<IndexWriter> writer = new SetOnce<>();
public SetOnce() {
set = new AtomicReference<>();
}
IndexWriterConfig setIndexWriter(IndexWriter writer) {
if (this.writer.get() != null) {
throw new IllegalStateException(
"do not share IndexWriterConfig instances across IndexWriters");
}
this.writer.set(writer);
return this;
}
获取文件锁
###############################################################################
public static final String WRITE_LOCK_NAME = "write.lock";
//在IndexWriter类中,定义了一个不可更改的lockName,使得无论哪个线程通过IndexWriter来获得索引文件锁时,
//lockName的值都是相同的,这样就能通过判断该lockName是否在locks容器中来实现互斥
public Lock obtainLock(Directory dir, String lockName) throws IOException {
synchronized (locks) {
if (locks.add(lockName)) {
return new SingleInstanceLock(lockName);
} else {
throw new LockObtainFailedException(
"lock instance already obtained: (dir=" + dir + ", lockName=" + lockName + ")");
}
}
}
2.获取封装后的Directory
LockValidatingDirectoryWrapper:
在对索引目录中的文件进行任意形式的具有"破坏性"(destructive)的文件系统操作(filesystem operation)前尽可能(best-effort)确保索引文件锁是有效的(valid)。
索引目录中的"破坏性"的文件系统操作包含下面几个内容:
- deleteFile(String name)方法:删除索引目录中的文件
- createOutput(String name, IOContext context)方法:在索引目录中创建新的文件
- copyFrom(Directory from, String src, String dest, IOContext context)方法:在索引目录中,将一个文件中的内容src复制到同一个索引目录中的另外一个不存在的文件dest
- rename(String source, String dest)方法:重命名索引目录中的文件
- syncMetaData()方法:磁盘同步操作
- sync(Collection names)方法:磁盘同步操作
至于为什么要保证这个,后续的过程中会揭晓。
public IndexWriter(Directory d, IndexWriterConfig conf) throws IOException {
boolean success = false;
try {
directoryOrig = d;
directory = new LockValidatingDirectoryWrapper(d, writeLock);
mergeScheduler = config.getMergeScheduler();
mergeScheduler.initialize(infoStream, directoryOrig);
OpenMode mode = config.getOpenMode();
}
}
3.根据不同的OpenMode执行对应的工作
图中可以看出,尽管Lucene提供了三种索引目录的打开模式,但实际上只有CREATE跟APPEND两种打开模式的逻辑。使用一个布尔值indexExists来描述流程点索引目录中是否已经存在旧的索引?,如果存在,那么indexExists的值为true,反之为false。indexExists在后面的流程中会被用到。下面我们分别介绍执行CREATE模式下的工作、执行APPEND模式下的工作这两个流程。
if (mode == OpenMode.CREATE) {
indexExists = DirectoryReader.indexExists(directory);
create = true;
} else if (mode == OpenMode.APPEND) {
indexExists = true;
create = false;
} else {
// CREATE_OR_APPEND - create only if an index does not exist
indexExists = DirectoryReader.indexExists(directory);
create = !indexExists;
}
3.1 create模式下的流程
配置检查
下面的代码描述的是在设置了配置IndexCommit之后对OpenMode进行配置检查,其中config指的是IndexWriter的配置信息IndexWriterConfig对象:
-
条件一:如果用户设置的OpenMode为CREATE,由于该模式的含义是生成新的索引或覆盖旧的索引,而设置IndexCommit的目的是读取已经有的索引信息,故这两种是相互冲突的逻辑,Lucene通过抛出异常的方法来告知用户不能这么配置
-
条件二:如果用户设置的OpenMode为CREATE_OR_APPEND,由于索引目录中是否已经存在旧的索引?
判断出indexExists的值为false,即索引目录中没有任何的提交,但用户又配置了IndexCommit,这说明用户配置的IndexCommit跟IndexWriter类的有参构造函数中的参数dictionary必须为同一个索引目录
if (config.getIndexCommit() != null) {
// We cannot both open from a commit point and create:
if (mode == OpenMode.CREATE) {
throw new IllegalArgumentException(
"cannot use IndexWriterConfig.setIndexCommit() with OpenMode.CREATE");
} else {
throw new IllegalArgumentException(
"cannot use IndexWriterConfig.setIndexCommit() when index has no commit");
}
}
初始化一个新的SegmentInfos对象,同步SegmentInfos的部分信息
final SegmentInfos sis = new SegmentInfos(config.getIndexCreatedVersionMajor());
if (indexExists) {
final SegmentInfos previous = SegmentInfos.readLatestCommit(directory);
sis.updateGenerationVersionAndCounter(previous);
}
segmentInfos = sis;
rollbackSegments = segmentInfos.createBackupSegmentInfos();
// Record that we have a change (zero out all
// segments) pending:
changed();
#########################################################################################
void updateGenerationVersionAndCounter(SegmentInfos other) {
updateGeneration(other);
this.version = other.version;
this.counter = other.counter;
}
如果索引目录中已经存在旧的索引,那么indexExists的值为true,那么我们先需要获得旧的索引中的最后一次提交commit中的SegmentInfos中的三个信息,即version、counter、generation:
-
version:该值用来描述SegmentInfos发生改变的次数,即索引信息发生改变的次数
-
counter:它跟下划线“_”作为一个组合值,用来描述下一次生成(commit、flush操作)的新段对应的索引文件的前缀值,下图中"_4"、"_5"的4、5即为counter值,该值为一个从0开始的递增值
-
generation:用来描述执行提交操作后生成的segments_N文件的N值,图10中,generation的值为2
防止覆盖:使得新生成的索引文件不会跟旧的索引文件有一样的名字,即不会覆盖旧的索引文件,那么其他线程可以正常通过IndexCommit读取旧索引执行搜索。
设置回滚点
List<SegmentCommitInfo> createBackupSegmentInfos() {
final List<SegmentCommitInfo> list = new ArrayList<>(size());
for (final SegmentCommitInfo info : this) {
assert info.info.getCodec() != null;
list.add(info.clone());
}
return list;
}
如果索引目录中存在旧的索引,那么另旧的索引对应的SegmentInfos对象中的segments对象赋值给回滚内容rrollbackSegments,否则rollbackSegments为null。在执行commit()的过程中,rollbackSegments会被更新为这次提交对应的segments对象。
3.2 APPEND模式工作流程
判断StandardDirectoryReader是否为空?
如果用户通过IndexWriterConfig.setIndexCommit(IndexCommit commit)设置了IndexCommit,那么Lucene会尝试根据该IndexCommit获得一个StandardDirectoryReader,它描述了IndexCommit中包含的索引信息:主要是SegmentInfos对象的信息。StandardDirectoryReader为空的情况有两种:
- 用户没有设置IndexCommit
- 用户设置了IndexCommit,但是IndexCommit中没有StandardDirectoryReader对象的信息
为什么IndexCommit中可能会没有StandardDirectoryReader对象的信息?
### ReaderCommit
static final class ReaderCommit extends IndexCommit {
private String segmentsFileName;
Collection<String> files;
Directory dir;
long generation;
final Map<String, String> userData;
private final int segmentCount;
private final StandardDirectoryReader reader; // 可以直接给出
}
#### CommitPoint
private static final class CommitPoint extends IndexCommit {
Collection<String> files;
String segmentsFileName;
boolean deleted;
Directory directoryOrig;
Collection<CommitPoint> commitsToDelete;
long generation;
final Map<String, String> userData;
private final int segmentCount;
}
# 装饰类,修饰CommitPoint
########### SnapshotCommitPoint
private class SnapshotCommitPoint extends IndexCommit {
/** The {@link IndexCommit} we are preventing from deletion. */
protected IndexCommit cp;
}
故如果是ReaderCommit对象,那么就可以获得StandardDirectoryReader对象,而ReaderCommit对象则是通过StandardDirectoryReader.getIndexCommit()方法获得,由于该方法的实现很简单,故直接给出:
## ReaderCommit 类
public IndexCommit getIndexCommit() throws IOException {
ensureOpen();
return new ReaderCommit(this, segmentInfos, directory);
}
可以看出,StandardDirectoryReader对象通过调用getIndexCommit()方法,构造了一个新的ReaderCommit对象,并且将自己(this指针)作为ReaderCommit的成员变量之一,即图5中红框标注。尽管无法通过CommitPoint对象中获得StandardDirectoryReader对象,但这里仍然要说下CommitPoint对象是什么生成的。
CommitPoint对象生成点有两处:
- 生成IndexFileDeleter对象期间:这个时机点即图1中的流程点生成对IndexFileDeleter,故我们这里先暂时不展开
- 执行commit()操作期间
Reader不为null:用StandardDirectoryReader初始化一个新的SegmentInfos对象
CREATE模式下的工作时说到,该模式下初始化一个新的SegmentInfos对象时,它不包含任何的索引信息,而在APPEND模式下,则是用StandardDirectoryReader中的索引信息来初始化一个新的SegmentInfos对象,即所谓的"追加"。
所以:
segmentInfos = reader.segmentInfos.clone();
Reader不为null:设置回滚点
上文中根据IndexCommit获得的StandardDirectoryReader,它包含的SegmentInfos在后面的流程中将会作为回滚内容,而在这个流程中,最重要的一步是检查SegmentInfos中包含的索引信息对应的索引文件是否还在索引目录中。
SegmentInfos对象是索引文件segments_N和索引文件.si在内存中的表示,图14中的提交中包含了两个段,即以_0跟_1开头的两个段,所以索引文件segments_1中有两个SegmentCommitInfo字段,接着根据SegmentCommitInfo中的SegName字段,该字段的值描述的是该段对应的所有索引文件的前缀值,即_0,那么就可以在索引目录中找到 索引文件_0.si,而在 索引文件_0.si的Files字段(图15中红框标注)中存储了其他索引文件的名字,同样地根据这些索引文件的名字在索引目录中读取到所有的索引信息。另外SegmentCommitInfo中的两个字段FieldInfosFiles、UpdatesFiles也是存储了索引文件的名字,当一个段中的DocValues发生变更时,变更的信息也用索引文件描述,并且索引文件的名字存储在这两个字段里。
public class SegmentCommitInfo {
private final Map<Integer, Set<String>> dvUpdatesFiles = new HashMap<>();
// TODO should we add .files() to FieldInfosFormat, like we have on
// LiveDocsFormat?
// track the fieldInfos update files
private final Set<String> fieldInfosFiles = new HashSet<>();
}
从上文的描述可以看出,尽管我们通过IndexCommit可以获得SegmentInfos信息,但是该对象只是描述了它对应的索引文件有哪些,并不具有这些索引文件真正的数据,故可能在获得IndexCommit之后,索引又发生了变化,例如又出现了新的提交,那么根据默认的索引删除策略,segments_1文件就会被删除,当执行回滚操作时就无法获得真正的索引数据。如果出现在这个情况,那么在当前流程点会抛出如下的异常:
throw new IllegalArgumentException("the provided reader is stale: its prior commit file \"" + segmentInfos.getSegmentsFileName() + "\" is missing from index");
Reader不为null:同步SegmentInfos以及回滚信息中SegmentInfos中的部分信息
为什么StandardDirectoryReader中可能没有IndexWriter对象?获得一个StandardDirectoryReader有几种方法,例如:
- 方法一:DirectoryReader.open(final Directory directory)
- 方法二:DirectoryReader.open(final IndexCommit indexCommit)
- 方法三:DirectoryReader.open(final IndexWriter indexWriter)
- 方法四:DirectoryReader.open(final IndexWriter indexWriter, boolean applyAllDeletes, boolean writeAllDeletes)
其中通过方法一和方法二获得的StandardDirectoryReader对象中是没有IndexWriter对象的,即使方法二的参数indexCommit对象中有IndexWriter对象。
为什么持有(引用)IndexWriter对象的StandardDirectoryReader需要执行图中的两个同步操作:
源码中是这么说的:
// In case the old writer wrote further segments (which we are now dropping)
该注释的详细意思就是:我们使用的IndexCommit参数对应的索引信息可能不是old writer最新的提交对应的索引信息,那么比IndexCommit更加新的的提交(一个或多个)都应该丢弃(dropping),为了能正确的处理那些应该被丢弃的段,我们需要上面图中的两个更新操作。
Reader为null:初始化新的SegmentInfo对象
于StandardDirectoryReader为空,那么就从索引目录中初始化一个新SegmentInfos对象(,即通过找到索引目录中的segments_N文件读取索引信息。
String lastSegmentsFile = SegmentInfos.getLastCommitSegmentsFileName(files);
if (lastSegmentsFile == null) {
throw new IndexNotFoundException(
"no segments* file found in " + directory + ": files: " + Arrays.toString(files));
}
// Do not use SegmentInfos.read(Directory) since the spooky
// retrying it does is not necessary here (we hold the write lock):
segmentInfos = SegmentInfos.readCommit(directoryOrig, lastSegmentsFile);
索引目录中有多个segments_N文件时该如何选择:Lucene设定为读取最新的一次提交,即选取segments_N的N值最大的那个,因为N越大意味着更新的提交(commit()操作)
Reader为null:IndexCommit 是否为null
StandardDirectoryReader为空的情况分为下面两种:
- 用户没有设置IndexCommit
- 用户设置了IndexCommit,但是IndexCommit中没有StandardDirectoryReader对象的信息
如果是第一种情况的进入到当前流程点,那么当前流程点的出口为是,那么以APPEND模式打开的IndexWriter追加的索引信息为索引目录中最新的一次提交。其他的则直接走到回滚点。
如果IndexCommit不为空,那么IndexCommit必定是CommitPoint或者SnapshotCommitPoint对象,接着就需要执行下面的配置检查:
if (commit.getDirectory() != directoryOrig) {
throw new IllegalArgumentException(
"IndexCommit's directory doesn't match my directory, expected="
+ directoryOrig
+ ", got="
+ commit.getDirectory());
}
其中commit即IndexCommit对象、directoryOrg为IndexWriter的工作目录,这个配置检查意味着要求当前构造的IndexWriter的工作目录必须和IndexCommit对应的索引信息所在的目录必须一致。
Reader为null:用IndexCommit更新SegmentInfos对象
SegmentInfos oldInfos =
SegmentInfos.readCommit(directoryOrig, commit.getSegmentsFileName());
segmentInfos.replace(oldInfos);
changed();
if (infoStream.isEnabled("IW")) {
infoStream.message(
"IW", "init: loaded commit \"" + commit.getSegmentsFileName() + "\"");
}
}
rollbackSegments = segmentInfos.createBackupSegmentInfos();
4.检查IndexSort合法性
如果设置了IndexSort,那么在生成一个段的过程中,Lucene会根据用户提供的排序规则对段内的文档进行排序, 如果用户通过IndexWriterConfig.setIndexSort(Sort sort)设置了IndexSort配置,那么需要对参数Sort进行合法性检查,检查逻辑如下所示:
想深入了解IndexSort的设置,则可以参考下面的流程:
private void validateIndexSort() {
Sort indexSort = config.getIndexSort();
if (indexSort != null) {
for (SegmentCommitInfo info : segmentInfos) {
Sort segmentIndexSort = info.info.getIndexSort();
if (segmentIndexSort == null || isCongruentSort(indexSort, segmentIndexSort) == false) {
throw new IllegalArgumentException(
"cannot change previous indexSort="
+ segmentIndexSort
+ " (from segment="
+ info
+ ") to new indexSort="
+ indexSort);
}
}
}
}
5.生成对象BufferedUpdatesStream,DocumentsWriter,ReaderPool对象
BufferedUpdatesStream
BufferedUpdatesStream用来追踪(track)FrozenBufferedUpdates,主要负责执行FrozenBufferedUpdates的两个工作:
- 获得nextGen:它用来描述FrozenBufferedUpdates中的删除信息应该作用哪些段
- 作用(apply)删除信息:FrozenBufferedUpdates中存放了删除信息以及更新信息(DocValues相关),为了方便描述,在下文中 删除信息、更新信息统称为删除信息。删除信息被作用到每一个段称为处理删除信息,根据作用(apply)的目标段,处理删除信息划分为两种处理方式:
- 全局FrozenBufferedUpdates:根据全局FrozenBufferedUpdates内的nextGen值,其删除信息将要作用到所有比该nextGen值小的段
- 段内FrozenBufferedUpdates:在生成索引文件的过程中,我们只处理了部分满足删除信息,即只处理了满足删除信息TermArrayNode、TermNode的段内部分文档,而如果段内FrozenBufferedUpdates还存在删除信息QueryArrayNode、DocValuesUpdatesNode,那么根据段内FrozenBufferedUpdates就可以找出所有剩余的满足删除的文档。
后续补充这一段逻辑处理。
DocumentsWriter
DocumentsWriter对象主要负责下面的三个工作:
- 文档的增删改:用户通过IndexWriter对象执行文档的增删改的任务,实际都是IndexWriter通过调用DocumentsWriter对象来实现的
- 将DWPT生成(flush)为一个段。
- 执行主动flush以后的收尾工作。
后续补充这一段逻辑处理。
ReaderPool
ReaderPool的命名方式就能完美描述该对象的作用,字面意思就是 存放reader的池子(pool),ReaderPool就是用来缓存SegmentReader对象(SegmentReader用来描述一个段的索引信息),使得Lucene在执行下面的操作时都会尝试先去ReaderPool取出SegmentReader:
-
作用(apply)删除信息、更新DocValues信息
对于索引目录中的某一个段,由于后续有新的删除/更新操作,如果该段中的文档满足删除/更新的条件,那么该段对应的SegmentReader中的索引信息也需要发生更改,那么根据索引信息是否会被更改可以分为下面两类:- 不会发生变更的索引信息:SegmentCoreReaders
- 会发生变更的索引信息:该索引信息即描述删除信息的索引文件.liv、描述域信息的索引文件.fnm、以及描述DocValues的索引文件.dvd&&.dvm
生成一个SegmentReader对象的开销是极大的,原因是读取索引信息为磁盘I/O操作,故使用ReaderPool来缓存SegmentReader,当需要作用(apply)删除信息、更新DocValues信息时,只需要从ReaderPool中取出该段对应的SegmentReader(如果不存在则先添加到ReaderPool),并且只修改SegmentReader中会发生变更的索引信息。在flush()阶段,DWPT被flush为一个段后,并不会马上被添加到ReaderPool中(lazy init机制),而是当该段需要被作用(apply)删除信息、更新DocValues信息时。
-
执行段的合并
执行段的合并的过程是通过每个段对应的SegmentReader中包含的索引信息进行合并,故在合并期间需要获取待合并段的SegmentReader,而获取的方式就是从ReaderPool获取。当然也有可能一个或多个待合并的段对应的SegmentReader并不在ReaderPool(原因是没有 作用(apply)删除信息、更新DocValues信息),那么此时就需要生成新的SegmentReader对象,并添加到ReaderPool中。 -
分发(handing out)一个实时的Reader
DirectoryReader.open(final IndexWriter indexWriter)
DirectoryReader.open(final IndexWriter indexWriter, boolean applyAllDeletes, boolean writeAllDeletes)
上述能获得具有NRT功能的StandardDirectoryReader,并且在这两个方法的实现过程中,会将StandardDirectoryReader中的SegmentReader缓存到ReaderPool中,这样的做法使得当再次通过上述两个方法或者性能更高的OpenIfChange()方法获得StandardDirectoryReader时,能先从ReaderPool中获得缓存的SegmentReader,即所谓的"分发"。
6.生成对象IndexFileDeleter
IndexFileDeleter用来追踪SegmentInfos是否还"活着(live)",简单的概括就是SegmentInfos对象是索引文件segments_N和索引文件.si在内存中的表示。
当执行索引删除策略时,例如默认的索引删除策略KeepOnlyLastCommitDeletionPolicy,新的提交生成后(即生成新的segments_N文件)需要删除上一次提交,即需要删除上一次提交对应的所有索引信息,而用来描述所有索引信息的正是SegmentInfos,删除SegmentInfos的真正目的是为了删除对应在索引目录中的索引文件,但这些索引文件如果正在被其他SegmentInfos引用,那么就不能被删除,IndexFileDeleter真正的工作就是判断索引目录中的索引文件是否允许被删除。
IndexFileDeleter如何判断索引目录中的索引文件是否允许被删除
使用引用计数的方式。
结束语
创建IndexWriter过程比较复杂,涉及到的操作较多,后续会对其中的某些过程细化。