本文档旨在分析Lucene如何把业务信息写到磁盘上的大致流程,并不涉及Document中每个Field如何存储(该部分放在另外一篇wiki中介绍)。
一,Lucene建索引API
二,创建IndexWriter
NIOFSDirectory.open() |
如果是64位JRE会得到MMapDirectory(采用内存映射的方式写索引数据到File中)。
可以对IndexWriter做一些属性配置,IndexWriterConfig里面有非常丰富的各种配置。
三,创建Document
这个步骤比较简单,主要是将业务字段组装成一个Document。一个Document由多个Field组成的。
每个Filed一般有四个属性组成:
- name:该字段的名称
- value:该字段的值
- value是否需要存储到索引文件中:如果存储到索引文件中,则search的时候可以从Document中读取到该字段的值
- value值是否被索引:如果该字段被索引,则可以通过该字段为条件进行检索
四,添加Document
添加一个Document,其实调用的是updateDocument。而Lucene更新Document不像Mysql可以直接更新某一条记录,所以只能先删除这条记录(Document),然后再添加上这条Document。下面参数Term,是一个检索条件,满足条件的Document做更新。
public void updateDocument(Term term, Iterable<? extends IndexableField> doc) throws IOException { ensureOpen(); try { boolean success = false; try { if (docWriter.updateDocument(doc, analyzer, term)) { processEvents(true, false); } success = true; } finally { if (!success) { if (infoStream.isEnabled("IW")) { infoStream.message("IW", "hit exception updating document"); } } } } catch (AbortingException | OutOfMemoryError tragedy) { tragicEvent(tragedy, "updateDocument"); } }
1 Lucene使用场景
这里从下面几个角度阐述下为什么Lucene不能直接更新一个Document?
- Lucene的设计本质是一个面向检索,或者面向读的系统。为了方面的检索,在建立索引的时候做了大量的读优化存储设计。简而言之,为了读的性能,牺牲了方便写、更新的操作。
- Lucene使用背景暗含了:Lucene适合(擅长)频繁读,不常写的场景。
所以上面添加一个Document,最后演变成了更新一个Document。并且updateDocument包含两个串行操作
(1)先检索,如果有满足条件的Document,则删除
(2)如果没有满足条件的Document,则直接添加到内存中
2 重要的几个基础类
在看docWriter.updateDocument(doc, analyzer, term)代码之前,我们先看几个Lucene子建的类,下面着重分析下:
2.1 DocumentsWriterPerThreadPool
Lucene内部实现的一个DocumentsWriterPerThread池(并不是严格意义的线程池),主要是
实现DocumentsWriterPerThread的重用(准确来说是实现ThreadState的重用)。该类可以简单理解一个线程池。
2.2 ThreadState
本质是个读写锁,用来配合DocumentsWriterPerThread来完成对一个Document的写操作。
2.3 DocumentsWriterPerThread
简单理解成一个Document的写线程。线程池保证了DocumentsWriterPerThread的重用。
2.4 DocumentsWriterFlushControl
控制DocumentsWriterPerThread完成index过程中flush操作
2.5 FlushPolicy
刷新策略
理解了ThreadState这个类应该就简单了,甚至可以直接把该类看做带读写锁控制的写线程。其实是ThreadState内部引用DocumentWriterPerThread实例。在线程池初始化的时候就创建了8个ThreadState(这个时候并没有初始化,意思是DocumentWriterPerThread并没有新建起来,而是延迟初始化具体线程)。后面就尽量重用这个8个ThreadState。
3 docWriter.updateDocument
好了,看完了几个基础类,回到上面updateDocument最关键的是这一行。
4 docWriter.updateDocument详细步骤
-
从线程池中获取一个ThreadState
- 初始化ThreadState的线程DocumentsWriterPerThread
- 该线程更新Document
- 该线程重新回到线程池中。线程池中维护了一个freeList,可重用的ThreadState都放到该freeList里面
5 DocumentsWriterPerThread.updateDocument详细步骤
该Document的更新交给一个DocumentsWriterPerThread之后,我们再往下看。
该线程里面我们只关心一行代码
consumer.processDocument(); |
从这里差不多就豁然开朗了,一切最后该Document的处理是交给了一个DocConsumer来处理。而这个DocConsumer的获取见下:
abstract DocConsumer getChain(DocumentsWriterPerThread documentsWriterPerThread) throws IOException; |
Lucene实现了一个默认的DocConsumer即:DefaultIndexingChain。 那接下来就看该DocConsumer是如何处理该Document的了就行了。
6 DefaultIndexingChain.processDocument详细步骤
看到上面代码,我笑了。哈哈,越来越清晰,有没有。对该Document的处理,无非就是演化成遍历每个Field,对Field做处理就行了。但是具体Field怎么处理,该wiki不涉及,放到另外一篇wiki中深入记录(参考:Document存储细节)。
五,Commit Document
indexWriter.commit(); |
提交Commit完成如下工作:
- 凡是挂起的改变都提交到index中。包括新增加的文档,要删除的文档,segement的合并。
- 该操作会执行Directory.sync,sync操作会将文件系统的cache都刷新到disk上面。虽然比较耗时(同步耗时),但是刷新到disk上之后,VM挂掉(或者断电)都不影响这些挂起的更新。
sync操作具体的解释可参考如下一段解释:
传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为延迟写(delayed write)(Bach [1986]第3章详细讨论了缓冲区高速缓存)。 延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。 sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。 通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。 fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。 fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。 对于提供事务支持的数据库,在事务提交时,都要确保事务日志(包含该事务所有的修改操作以及一个提交记录)完全写到硬盘上,才认定事务提交成功并返回给应用层。
看完这段解释就能明白,sync操作就是将文件系统(甚至内核)中的缓存数据都刷新到disk上面,保证数据的安全性(OS挂掉,断电,数据不会丢失)。
那具体Lucene做了些什么呢?
走到prepareCommitInternal里面就是详细的刷新操作,索引刷新操作放在另外一篇wiki中介绍。
六,关闭IndexWriter
刷新数据,关闭资源。往里走,逻辑还是很丰富的。等flush详细讲完之后,再回头看这部分。