在上文中,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相关信息持久化的问题。
我们先来看看AddIndex类的execute方法:
public void execute(MultiIndex index) throws IOException { PersistentIndex idx = index.getOrCreateIndex(indexName); if (!index.indexNames.contains(indexName)) { index.indexNames.addName(indexName); // now that the index is in the active list let the merger know about it index.merger.indexAdded(indexName, idx.getNumDocuments()); } }
public void execute(MultiIndex index) throws IOException { // get index if it exists for (Iterator it = index.indexes.iterator(); it.hasNext();) { PersistentIndex idx = (PersistentIndex) it.next(); if (idx.getName().equals(indexName)) { idx.close(); index.deleteIndex(idx); break; } } }
synchronized void deleteIndex(PersistentIndex index) { // remove it from the lists if index is registered indexes.remove(index); indexNames.removeName(index.getName()); // during recovery it may happen that an index had already been marked // deleted, so we need to check if it is already marked deleted. synchronized (deletable) { if (!deletable.contains(index.getName())) { deletable.addName(index.getName()); } } }
/** * Names of active persistent index directories. */ private final IndexInfos indexNames = new IndexInfos("indexes"); /** * Names of index directories that can be deleted. */ private final IndexInfos deletable = new IndexInfos("deletable");
这样我们就明白了,indexs和deletable原来是同一种对象。而且可以肯定,IndexInfos这个类具有持久化的功能,它需要把自身包含的数据持久化到磁盘上。
接着我们来看看这个类中有些什么东西:
class IndexInfos { /** * For new segment names. */ private int counter = 0; /** * Flag that indicates if index infos needs to be written to disk. */ private boolean dirty = false; /** * List of index names */ private List indexes = new ArrayList(); /** * Set of names for quick lookup. */ private Set names = new HashSet(); ……… }
void addName(String name) { if (names.contains(name)) { throw new IllegalArgumentException("already contains: " + name); } indexes.add(name); names.add(name); dirty = true; }
void write(File dir) throws IOException { // do not write if not dirty if (!dirty) { /*目录没有变化,直接返回*/ return; } /*有新的数据添加进来,需要持久化了,创建一个新文件indexs.new */ File nu = new File(dir, name + ".new"); OutputStream out = new FileOutputStream(nu); try { DataOutputStream dataOut = new DataOutputStream(out); /*前4个byte写入new segment names, 但是我没有看到这个变量再什么地方被使用*/ dataOut.writeInt(counter); /*前5-8个byte写入目录总数*/ dataOut.writeInt(indexes.size()); for (int i = 0; i < indexes.size(); i++) { /*写入每个indexName*/ dataOut.writeUTF(getName(i)); } } finally { out.close(); } // delete old File old = new File(dir, name); if (old.exists() && !old.delete()) { throw new IOException("Unable to delete file: " + old.getAbsolutePath()); } /*删除索引目录中的indexs文件,并将indexs.new改名成indexs*/ if (!nu.renameTo(old)) { throw new IOException("Unable to rename file: " + nu.getAbsolutePath()); } dirty = false; }
void read(File dir) throws IOException { InputStream in = new FileInputStream(new File(dir, name)); try { DataInputStream di = new DataInputStream(in); counter = di.readInt(); for (int i = di.readInt(); i > 0; i--) { String indexName = di.readUTF(); indexes.add(indexName); names.add(indexName); } } finally { in.close(); } }
MultiIndex(File indexDir, SearchIndex handler, Set excludedIDs, NamespaceMappings mapping) throws IOException { this.indexDir = indexDir; this.handler = handler; this.cache = new DocNumberCache(handler.getCacheSize()); this.redoLog = new RedoLog(new File(indexDir, REDO_LOG)); this.excludedIDs = new HashSet(excludedIDs); this.nsMappings = mapping; if (indexNames.exists(indexDir)) { /*读取有效的目录*/ indexNames.read(indexDir); } if (deletable.exists(indexDir)) { /*读取无效的目录信息*/ deletable.read(indexDir); } // try to remove deletable files if there are any /*删除之*/ attemptDelete(); ………. }
通过这种方式,jackrabbit就可以保证在程序在不执行添加索引,或者索引合并(因为这两个操作中都有AddIndex和DeleteIndex被执行,也就是说这个两个操作都会导致目录变更)的时候突然拓机的情况下,程序重启还能正常提供服务。
如果程序正在执行merge操作,产生了新目录,需要删除老的目录,这个时候情况比较麻烦:
1在这两个信息没有被持久化到磁盘上之前程序歇菜了,那可能还好办,毕竟原始的数据还在磁盘上,不过产生的新目录不能被读取到,因为不在indexes文件里。
2但是indexNames持久化成功,deletable持久化失败,那就没有办法了,这样就会导致这些个需要删除的目录信息不存在于deletable中,而新的有效目录也存在于indexNames中,那么程序重启的时候能读到这个目录,但是不知道哪些目录需要被删除。
在这样的场景下会产生一些冗余目录和冗余文件,但是不影响正常数据,后面会讲到redolog和indexes的关系,很重要,是保证数据完整性的重要一步。
总结
真相如此简单,但是却不得不考虑,由此证明,写代码,写框架,尤其数据库之类的东西,重要的是逻辑的严谨性,最重要的还是逻辑的严谨性,如同设计模式这类的东西只是辅助技巧,切不可舍本求末,亦不可舍主求次。主次分明才是最好的平衡。
To be continue