在上文中,ahuaxuan讲到了索引创建的主体流程,但是索引合并其实还有一个较为重要的细节ahuaxuan没有详细阐述。本文中,ahuaxuan将会详细阐述这个问题
本文分成两部分内容
1 考虑应用拓机时的数据正确性问题。
2 jackrabbit是如何解决这些问题的。
而这个细节将会直接影响我们对query module的改造,这个细节虽然不难,但是却很重要,是jackrabbit中一个比较重要的设计。下面让我们一起来看看这个是什么样的细节。
回顾上文,我们知道一个目录合并的主要逻辑是10个以上同层次(一共10个层次,还记得否)的小目录会合并成上一个层次的目录
那么现在的问题是
1. 当低层次的多个小目录合并完成一个高层次的目录之后,我们需要把这些目录删除。
2. 并且要通知程序产生了一个新的目录。
但是这个时候程序突然挂掉,怎么办呢。那么就必须有一个恢复机制。
需要被删除或者需要增加的目录的信息如果没有持久化的机制,那么程序再启动的时候就无法分辨哪些索引数据是要删除的,哪些是新增的。咋办呢,咋整呢?我们得有一个持久化的机制来证明哪些索引目录是需要被删除,哪些是有效的索引目录,这样Repository启动的时候就可以拿到正确的IndexReader。
Ahuaxuan在前面的文章分析过,Action的接口以及其实现类:
其中AddIndex和DeleteIndex这两个Action值的关注,这两个Action一个是创建PersistentIndex,一个是删除PersistentIndex,前面讲过一个PersistentIndex对应一个索引目录,AddIndex和DeleteIndex中必然包含着PersistentIndex相关信息持久化的问题。
再来看看DeleteIndex的execute方法:
进入这个方法后,我们可以看到,所谓的删除,就是把indexNames中的PersistentIndex的name删除掉,并把这个要删除的indexName加入到deletable中。
关键是indexNames和deletable到底是个什么东西,从这里我们可以看出来,其实indexNames和deletable就记录着PersistentIndex的name,也就是说这两个对象中保存着有效的索引目录和需要被删除的索引目录。当10个目录合并成一个目录的时候,就是把10个目录的name从indexNames中删除,并加入到deletable中去。
Jackrabbit就是通过这种方式来保证应用拓机时索引数据的正确性。
接下来,我们来看看deletable和indexNames到底是什么对象:
这样我们就明白了,indexs和deletable原来是同一种对象。而且可以肯定,IndexInfos这个类具有持久化的功能,它需要把自身包含的数据持久化到磁盘上。
接着我们来看看这个类中有些什么东西:
从这段代码看来,似乎这个类里面只有一个indexes是我们已知的(names是为了快速判断一个indexname是否在indexs这个list中),而且我们没有看到持久化的相关信息,抱着这样的想法,我们继续往下看,下面我们再来看看如何把一个PersistentIndex的数据indexName加入这个类中:
还是没有持久化的信息,ahuaxuan很焦虑,肾上腺激素含量开始升高,啥都别说了,接着看吧,1秒钟之后,终于发现这个方法:
由此可见,IndexInfos确实有把新添加的PersistentIndex对应的目录持久化起来,什么时候做这件事情呢,当然是在添加索引介绍的时候,比如说flush的时候,没错,就是前面讲到的multiIndex#update中的三大方法中的flush(记住它的触发条件哦),flush的时候,内存中的有效的索引目录的信息就会被持久化到磁盘上。
同样的道理,deletable中也是这样的逻辑,要删除的目录也会被持久化起来。
既然保存下来了,我们不妨看看什么时候会用到,于是乎查看read方法:
果不其然,有这么一个read方法,这个方法就是负责解析文件,并把文件中的数据拿出来放到内存中。那么这个方法是谁来调用的呢:ctrl+shift+g.
发现在MultiIndex的构造方法里确实用到了:
通过这种方式,jackrabbit就可以保证在程序在不执行添加索引,或者索引合并(因为这两个操作中都有AddIndex和DeleteIndex被执行,也就是说这个两个操作都会导致目录变更)的时候突然拓机的情况下,程序重启还能正常提供服务。
如果程序正在执行merge操作,产生了新目录,需要删除老的目录,这个时候情况比较麻烦:
1在这两个信息没有被持久化到磁盘上之前程序歇菜了,那可能还好办,毕竟原始的数据还在磁盘上,不过产生的新目录不能被读取到,因为不在indexes文件里。
2但是indexNames持久化成功,deletable持久化失败,那就没有办法了,这样就会导致这些个需要删除的目录信息不存在于deletable中,而新的有效目录也存在于indexNames中,那么程序重启的时候能读到这个目录,但是不知道哪些目录需要被删除。
在这样的场景下会产生一些冗余目录和冗余文件,但是不影响正常数据,后面会讲到redolog和indexes的关系,很重要,是保证数据完整性的重要一步。
总结
真相如此简单,但是却不得不考虑,由此证明,写代码,写框架,尤其数据库之类的东西,重要的是逻辑的严谨性,最重要的还是逻辑的严谨性,如同设计模式这类的东西只是辅助技巧,切不可舍本求末,亦不可舍主求次。主次分明才是最好的平衡。
本文分成两部分内容
1 考虑应用拓机时的数据正确性问题。
2 jackrabbit是如何解决这些问题的。
而这个细节将会直接影响我们对query module的改造,这个细节虽然不难,但是却很重要,是jackrabbit中一个比较重要的设计。下面让我们一起来看看这个是什么样的细节。
回顾上文,我们知道一个目录合并的主要逻辑是10个以上同层次(一共10个层次,还记得否)的小目录会合并成上一个层次的目录
那么现在的问题是
1. 当低层次的多个小目录合并完成一个高层次的目录之后,我们需要把这些目录删除。
2. 并且要通知程序产生了一个新的目录。
但是这个时候程序突然挂掉,怎么办呢。那么就必须有一个恢复机制。
需要被删除或者需要增加的目录的信息如果没有持久化的机制,那么程序再启动的时候就无法分辨哪些索引数据是要删除的,哪些是新增的。咋办呢,咋整呢?我们得有一个持久化的机制来证明哪些索引目录是需要被删除,哪些是有效的索引目录,这样Repository启动的时候就可以拿到正确的IndexReader。
Ahuaxuan在前面的文章分析过,Action的接口以及其实现类:
其中AddIndex和DeleteIndex这两个Action值的关注,这两个Action一个是创建PersistentIndex,一个是删除PersistentIndex,前面讲过一个PersistentIndex对应一个索引目录,AddIndex和DeleteIndex中必然包含着PersistentIndex相关信息持久化的问题。
我们先来看看AddIndex类的execute方法:
1.public void execute(MultiIndex index) throws IOException {
2.
3. PersistentIndex idx = index.getOrCreateIndex(indexName);
4.
5. if (!index.indexNames.contains(indexName)) {
6.
7. index.indexNames.addName(indexName);
8.
9. // now that the index is in the active list let the merger know about it
10.
11. index.merger.indexAdded(indexName, idx.getNumDocuments());
12.
13. }
14.
15. }
再来看看DeleteIndex的execute方法:
1.public void execute(MultiIndex index) throws IOException {
2.
3. // get index if it exists
4.
5. for (Iterator it = index.indexes.iterator(); it.hasNext();) {
6.
7. PersistentIndex idx = (PersistentIndex) it.next();
8.
9. if (idx.getName().equals(indexName)) {
10.
11. idx.close();
12.
13. index.deleteIndex(idx);
14.
15. break;
16.
17. }
18.
19. }
20.
21. }
1.synchronized void deleteIndex(PersistentIndex index) {
2.
3. // remove it from the lists if index is registered
4.
5. indexes.remove(index);
6.
7. indexNames.removeName(index.getName());
8.
9. // during recovery it may happen that an index had already been marked
10.
11. // deleted, so we need to check if it is already marked deleted.
12.
13. synchronized (deletable) {
14.
15. if (!deletable.contains(index.getName())) {
16.
17. deletable.addName(index.getName());
18.
19. }
20.
21. }
22.
23.}
进入这个方法后,我们可以看到,所谓的删除,就是把indexNames中的PersistentIndex的name删除掉,并把这个要删除的indexName加入到deletable中。
关键是indexNames和deletable到底是个什么东西,从这里我们可以看出来,其实indexNames和deletable就记录着PersistentIndex的name,也就是说这两个对象中保存着有效的索引目录和需要被删除的索引目录。当10个目录合并成一个目录的时候,就是把10个目录的name从indexNames中删除,并加入到deletable中去。
Jackrabbit就是通过这种方式来保证应用拓机时索引数据的正确性。
接下来,我们来看看deletable和indexNames到底是什么对象:
1./**
2. * Names of active persistent index directories.
3. */
4.private final IndexInfos indexNames = new IndexInfos("indexes");
5.
6. /**
7. * Names of index directories that can be deleted.
8. */
9.private final IndexInfos deletable = new IndexInfos("deletable");
10.
11.
这样我们就明白了,indexs和deletable原来是同一种对象。而且可以肯定,IndexInfos这个类具有持久化的功能,它需要把自身包含的数据持久化到磁盘上。
接着我们来看看这个类中有些什么东西:
1.class IndexInfos {
2.
3. /**
4. * For new segment names.
5. */
6. private int counter = 0;
7.
8. /**
9. * Flag that indicates if index infos needs to be written to disk.
10. */
11.
12. private boolean dirty = false;
13.
14. /**
15. * List of index names
16. */
17. private List indexes = new ArrayList();
18.
19. /**
20. * Set of names for quick lookup.
21. */
22. private Set names = new HashSet();
23.
24.………
25.}
从这段代码看来,似乎这个类里面只有一个indexes是我们已知的(names是为了快速判断一个indexname是否在indexs这个list中),而且我们没有看到持久化的相关信息,抱着这样的想法,我们继续往下看,下面我们再来看看如何把一个PersistentIndex的数据indexName加入这个类中:
1.void addName(String name) {
2.
3. if (names.contains(name)) {
4.
5. throw new IllegalArgumentException("already contains: " + name);
6.
7. }
8.
9. indexes.add(name);
10.
11. names.add(name);
12.
13. dirty = true;
14.
15. }
还是没有持久化的信息,ahuaxuan很焦虑,肾上腺激素含量开始升高,啥都别说了,接着看吧,1秒钟之后,终于发现这个方法:
1.void write(File dir) throws IOException {
2.
3. // do not write if not dirty
4.
5. if (!dirty) {
6.
7./*目录没有变化,直接返回*/
8.
9. return;
10. }
11.
12. /*有新的数据添加进来,需要持久化了,创建一个新文件indexs.new */
13.
14. File nu = new File(dir, name + ".new");
15.
16.
17.
18. OutputStream out = new FileOutputStream(nu);
19.
20. try {
21.
22. DataOutputStream dataOut = new DataOutputStream(out);
23.
24./*前4个byte写入new segment names, 但是我没有看到这个变量再什么地方被使用*/
25.
26. dataOut.writeInt(counter);
27.
28./*前5-8个byte写入目录总数*/
29.
30. dataOut.writeInt(indexes.size());
31.
32. for (int i = 0; i < indexes.size(); i++) {
33.
34./*写入每个indexName*/
35. dataOut.writeUTF(getName(i));
36.
37. }
38. } finally {
39. out.close();
40. }
41.
42. // delete old
43. File old = new File(dir, name);
44. if (old.exists() && !old.delete()) {
45. throw new IOException("Unable to delete file: " + old.getAbsolutePath());
46. }
47.
48./*删除索引目录中的indexs文件,并将indexs.new改名成indexs*/
49. if (!nu.renameTo(old)) {
50. throw new IOException("Unable to rename file: " + nu.getAbsolutePath());
51. }
52. dirty = false;
53.
54. }
由此可见,IndexInfos确实有把新添加的PersistentIndex对应的目录持久化起来,什么时候做这件事情呢,当然是在添加索引介绍的时候,比如说flush的时候,没错,就是前面讲到的multiIndex#update中的三大方法中的flush(记住它的触发条件哦),flush的时候,内存中的有效的索引目录的信息就会被持久化到磁盘上。
同样的道理,deletable中也是这样的逻辑,要删除的目录也会被持久化起来。
既然保存下来了,我们不妨看看什么时候会用到,于是乎查看read方法:
1.void read(File dir) throws IOException {
2.
3. InputStream in = new FileInputStream(new File(dir, name));
4.
5. try {
6. DataInputStream di = new DataInputStream(in);
7. counter = di.readInt();
8. for (int i = di.readInt(); i > 0; i--) {
9.
10. String indexName = di.readUTF();
11.
12. indexes.add(indexName);
13.
14. names.add(indexName);
15.
16. }
17. } finally {
18. in.close();
19. }
20. }
果不其然,有这么一个read方法,这个方法就是负责解析文件,并把文件中的数据拿出来放到内存中。那么这个方法是谁来调用的呢:ctrl+shift+g.
发现在MultiIndex的构造方法里确实用到了:
1.MultiIndex(File indexDir,
2.
3. SearchIndex handler,
4.
5. Set excludedIDs,
6.
7. NamespaceMappings mapping) throws IOException {
8.
9.
10.
11. this.indexDir = indexDir;
12. this.handler = handler;
13. this.cache = new DocNumberCache(handler.getCacheSize());
14. this.redoLog = new RedoLog(new File(indexDir, REDO_LOG));
15. this.excludedIDs = new HashSet(excludedIDs);
16. this.nsMappings = mapping;
17.
18. if (indexNames.exists(indexDir)) {
19./*读取有效的目录*/
20. indexNames.read(indexDir);
21.
22. }
23.
24. if (deletable.exists(indexDir)) {
25./*读取无效的目录信息*/
26. deletable.read(indexDir);
27. }
28.
29. // try to remove deletable files if there are any
30.
31./*删除之*/
32. attemptDelete();
33.……….
34.}
通过这种方式,jackrabbit就可以保证在程序在不执行添加索引,或者索引合并(因为这两个操作中都有AddIndex和DeleteIndex被执行,也就是说这个两个操作都会导致目录变更)的时候突然拓机的情况下,程序重启还能正常提供服务。
如果程序正在执行merge操作,产生了新目录,需要删除老的目录,这个时候情况比较麻烦:
1在这两个信息没有被持久化到磁盘上之前程序歇菜了,那可能还好办,毕竟原始的数据还在磁盘上,不过产生的新目录不能被读取到,因为不在indexes文件里。
2但是indexNames持久化成功,deletable持久化失败,那就没有办法了,这样就会导致这些个需要删除的目录信息不存在于deletable中,而新的有效目录也存在于indexNames中,那么程序重启的时候能读到这个目录,但是不知道哪些目录需要被删除。
在这样的场景下会产生一些冗余目录和冗余文件,但是不影响正常数据,后面会讲到redolog和indexes的关系,很重要,是保证数据完整性的重要一步。
总结
真相如此简单,但是却不得不考虑,由此证明,写代码,写框架,尤其数据库之类的东西,重要的是逻辑的严谨性,最重要的还是逻辑的严谨性,如同设计模式这类的东西只是辅助技巧,切不可舍本求末,亦不可舍主求次。主次分明才是最好的平衡。