作者:ahuaxuan
在前面的文章中,我们对jackrabbit做索引的流程有了较为深刻的认识,这个过程中包含了很多的特性,比如多线程作内存索引啊,文件系统的目录算法啊,文本提取的策略等等,在本文中,ahuaxuan将会继续描述jackrabbit在索引过程中的另一个特性。
Redolog是jackrabbit中保证数据一致性的又一个特色。
本文将会阐述以下几点内容:
1. Redolog的存储方式
2. Redolog和indexes还有deletable这两个文件之前的亲密关系
3. Redolog的恢复方式
Redolog的存储介质。
其实在前面的文章中,我们已经接触过它了,之前我们在执行Action的时候,总是会进入这个方法multiIndex#executeAndLog(Action a):
private Action executeAndLog(Action a)
throws IOException {
a. execute(this);
/*从这个代码,我们可以看出,每执行一个Action,都会将这个action追加到redoLog中*/
redoLog.append(a);
// please note that flushing the redo log is only required on
// commit, but we also want to keep track of new indexes for sure.
// otherwise it might happen that unused index folders are orphaned
// after a crash.
if (a.getType() == Action.TYPE_COMMIT || a.getType() == Action.TYPE_ADD_INDEX) {
redoLog.flush();
// also flush indexing queue
indexingQueue.commit();
}
return a;
}
从上面的代码中,我们得到了一个重要的信息,redolog有append和flush的功能,没啥好说的,其实就是一个文件操作。
关键的在于什么时候把数据写到磁盘上,这个才是重点。从上面的代码中看到,只有是事务提交Action.TYPE_COMMIT或者添加一个索引目录Action.TYPE_ADD_INDEX的时候才会把redolog中的数据刷到磁盘上。
这里面有两个原因,第一个原因是缓存的问题,由于VolatileIndex的存在,导致很多新的document对应的索引数据其实是放在内存中的,这一点在前面的文章中有详细的描述,那么一旦crash,内存中的数据毫无疑问会消失,索引线程成功的将document的索引数据生成在内存中,该索引线程成功返回,这个时候当机,那么这条索引线程之前所做的操作就白费了,所以就需要在索引线程返回之前,将内存中的对应记录放在文件中。这样即使当机,重启之后依然能否通过redolog恢复之前因为当机而消失的索引数据(如果索引线程在做索引的过程中当机,比如说代码还没有运行到redolog的时候当机,这种case神仙也救不了,认了吧)。
第二个原因是因为indexes文件,在前面的索引合并逻辑中,我们详细的讲过,每次合并都会产生新的目录,那么考虑下面这个场景:
1 VolatileIndex中index数据对应的document超过100个
2创建一个PersistentIndex(同时也创建了一个目录),并将这个新建的目录情况加入到redolog,但是并不将其刷到磁盘
3将VolatileIndex中的数据拷贝到PersistentIndex
4 将这个新的PersistentIndex加入到indexes文件中(该目录保存着所有有效索引目录的列表,一旦机器crash,那么就可以根据这个文件中包含的目录来打开indexreader),虽然说加进去,但是这个时候还没有刷到磁盘上,也只是加在内存中。
5 将这次新增的PersistentIndex加入到redolog。并将redolog的数据加刷到磁盘
6或提交事务(之所以”或”,是因为每次新建目录不一定提交事务,因为一个事务中是可以创建多个目录的),将新增的node索引请求记录到redolog,并将redolog的数据刷到磁盘
7 或进入flush,将indexes在内存中的数据刷到磁盘上。
8 在flush中clear redolog
如果我们将第5,6步去掉,那么在4和7步之间当机,那么新增的这个目录则不会记录在indexes对应的文件中,应用程序则不知道这个目录的存在。这个目录就变成了一个没爹没娘的孩子了。(当然,如果我们加上第5步,那么在4-5直接crash,还是有可能产生孤子目录.不过这样的几率非常小,因为4-5之间几乎没有什么逻辑,但是5-8的步骤中存在很多逻辑,相对来讲crash的可能性大很多)
而如果假设,存在第5步,在5和8之间出现了crash,那么显然,indexes对应在磁盘上的文件中是会有这个新的PersistentIndex所对应的目录存在的。那么这个时候当机也不要紧,redolog重做的时候会找到错误的目录,然后将这个错误的目录销毁。事实上,一次MultiIndex#update方法调用的过程中,假设传进来500个nodeid,那么其实在这次操作中,磁盘上会多出5个目录来(默认的minimergefactor=100),那么一旦第四次出错,导致事务不完整,但是前面4个目录就变成了脏目录了。那么在重启应用的时候应该把这个不完整事务带来的脏目录都删除掉。
讲完redolog的作用,存储类型以及和indexes直接的关系之后,我们来看看如何利用这个redolog进行recovery。
Recovery的逻辑集中在Recovery这个类中,在MultiIndex被创建的时候,会先新建这个类,然后通过读取redolog文件的方式来进行之前当机的recovery操作。其主要逻辑在Recovery#run方法中。
private void run() throws IOException {
List actions = redoLog.getActions();
/*找到所有只有开始没有结束的事务,并将其加入到losers这个集合中*/
for (Iterator it = actions.iterator(); it.hasNext();) {
MultiIndex.Action a = (MultiIndex.Action) it.next();
if (a.getType() == MultiIndex.Action.TYPE_START) {
losers.add(new Long(a.getTransactionId()));
} else if (a.getType() == MultiIndex.Action.TYPE_COMMIT) {
losers.remove(new Long(a.getTransactionId()));
}
}
/*找到最后一次成功提交的事务在redolog中的位置*/
int lastSafeVolatileCommit = -1;
Set transactionIds = new HashSet();
for (int i = 0; i < actions.size(); i++) {
MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
if (a.getType() == MultiIndex.Action.TYPE_COMMIT) {
transactionIds.clear();
} else if (a.getType() == MultiIndex.Action.TYPE_VOLATILE_COMMIT) {
transactionIds.retainAll(losers);
// check if transactionIds contains losers
if (transactionIds.size() > 0) {
// found dirty volatile commit
break;
} else {
lastSafeVolatileCommit = i;
}
} else {
transactionIds.add(new Long(a.getTransactionId()));
}
}
/*最后一次成功的事务之后的数据遍历一下,找到脏目录,脏目录是指在一次事务中创建的目录,但是事务最终没有被完整执行,那么这些目录应该被删除掉*/
for (int i = lastSafeVolatileCommit + 1; i < actions.size(); i++) {
MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
if (a.getType() == MultiIndex.Action.TYPE_CREATE_INDEX) {
a.undo(index);
}
}
/*将最后一次正确的事务之前的操作都重新做一遍*/
for (int i = 0; i < actions.size() && i <= lastSafeVolatileCommit; i++) {
MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
switch (a.getType()) {
case MultiIndex.Action.TYPE_ADD_INDEX:
case MultiIndex.Action.TYPE_CREATE_INDEX:
case MultiIndex.Action.TYPE_DELETE_INDEX:
case MultiIndex.Action.TYPE_DELETE_NODE:
// ignore actions by the index merger.
// the previously created index of a merge has been
// deleted because it was considered dirty.
// we are conservative here and let the index merger do
// its work again.
if (a.getTransactionId() == MultiIndex.Action.INTERNAL_TRANS_REPL_INDEXES) {
continue;
}
a.execute(index);
}
}
// now replay the rest until we encounter a loser transaction
for (int i = lastSafeVolatileCommit + 1; i < actions.size(); i++) {
MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
if (losers.contains(new Long(a.getTransactionId()))) {
break;
} else {
// ignore actions by the index merger.
if (a.getTransactionId() == MultiIndex.Action.INTERNAL_TRANS_REPL_INDEXES) {
continue;
}
a.execute(index);
}
}
// now we are consistent again -> flush
index.flush();
index.closeMultiReader();
}
从上面的这段代码可以看出,如果一个事务不完整,那么这个事务的操作将会全部回滚,而事务完整的操作将会被全部重做。
通过这些操作,redolog中的信息被解析出来,并执行,那么就可以找回丢失的数据,并且过滤掉错误的数据。
总结:redolog很重要。
TO BE CONTINUE