深入浅出jcr之十 redolog 和 recovery.docx

在前面的文章中,我们对jackrabbit做索引的流程有了较为深刻的认识,这个过程中包含了很多的特性,比如多线程作内存索引啊,文件系统的目录算法啊,文本提取的策略等等,在本文中,ahuaxuan将会继续描述jackrabbit在索引过程中的另一个特性。

Redolog是jackrabbit中保证数据一致性的又一个特色。
本文将会阐述以下几点内容:
1. Redolog的存储方式
2. Redolog和indexes还有deletable这两个文件之前的亲密关系
3. Redolog的恢复方式

Redolog的存储介质。

其实在前面的文章中,我们已经接触过它了,之前我们在执行Action的时候,总是会进入这个方法multiIndex#executeAndLog(Action a):

1.private Action executeAndLog(Action a)
2.            throws IOException {
3.a.  execute(this);
4./*从这个代码,我们可以看出,每执行一个Action,都会将这个action追加到redoLog中*/
5.        redoLog.append(a);
6.        // please note that flushing the redo log is only required on
7.        // commit, but we also want to keep track of new indexes for sure.
8.        // otherwise it might happen that unused index folders are orphaned
9.        // after a crash.
10.        if (a.getType() == Action.TYPE_COMMIT || a.getType() == Action.TYPE_ADD_INDEX) {
11.            redoLog.flush();
12.            // also flush indexing queue
13.            indexingQueue.commit();
14.        }
15.        return a;
16.}

从上面的代码中,我们得到了一个重要的信息,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方法中。

1.private void run() throws IOException {
2.        List actions = redoLog.getActions();
3.
4.        /*找到所有只有开始没有结束的事务,并将其加入到losers这个集合中*/
5.        for (Iterator it = actions.iterator(); it.hasNext();) {
6.            MultiIndex.Action a = (MultiIndex.Action) it.next();
7.            if (a.getType() == MultiIndex.Action.TYPE_START) {
8.                losers.add(new Long(a.getTransactionId()));
9.            } else if (a.getType() == MultiIndex.Action.TYPE_COMMIT) {
10.                losers.remove(new Long(a.getTransactionId()));
11.            }
12.        }
13.
14.        /*找到最后一次成功提交的事务在redolog中的位置*/
15.        int lastSafeVolatileCommit = -1;
16.        Set transactionIds = new HashSet();
17.        for (int i = 0; i < actions.size(); i++) {
18.            MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
19.            if (a.getType() == MultiIndex.Action.TYPE_COMMIT) {
20.                transactionIds.clear();
21.            } else if (a.getType() == MultiIndex.Action.TYPE_VOLATILE_COMMIT) {
22.                transactionIds.retainAll(losers);
23.                // check if transactionIds contains losers
24.                if (transactionIds.size() > 0) {
25.                    // found dirty volatile commit
26.                    break;
27.                } else {
28.                    lastSafeVolatileCommit = i;
29.                }
30.            } else {
31.                transactionIds.add(new Long(a.getTransactionId()));
32.            }
33.        }
34.
35.        /*最后一次成功的事务之后的数据遍历一下,找到脏目录,脏目录是指在一次事务中创建的目录,但是事务最终没有被完整执行,那么这些目录应该被删除掉*/
36.        for (int i = lastSafeVolatileCommit + 1; i < actions.size(); i++) {
37.            MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
38.            if (a.getType() == MultiIndex.Action.TYPE_CREATE_INDEX) {
39.                a.undo(index);
40.            }
41.        }
42.
43.        /*将最后一次正确的事务之前的操作都重新做一遍*/
44.        for (int i = 0; i < actions.size() && i <= lastSafeVolatileCommit; i++) {
45.            MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
46.            switch (a.getType()) {
47.                case MultiIndex.Action.TYPE_ADD_INDEX:
48.                case MultiIndex.Action.TYPE_CREATE_INDEX:
49.                case MultiIndex.Action.TYPE_DELETE_INDEX:
50.                case MultiIndex.Action.TYPE_DELETE_NODE:
51.                    // ignore actions by the index merger.
52.                    // the previously created index of a merge has been
53.                    // deleted because it was considered dirty.
54.                    // we are conservative here and let the index merger do
55.                    // its work again.
56.                    if (a.getTransactionId() == MultiIndex.Action.INTERNAL_TRANS_REPL_INDEXES) {
57.                        continue;
58.                    }
59.                    a.execute(index);
60.            }
61.        }
62.
63.        // now replay the rest until we encounter a loser transaction
64.        for (int i = lastSafeVolatileCommit + 1; i < actions.size(); i++) {
65.            MultiIndex.Action a = (MultiIndex.Action) actions.get(i);
66.            if (losers.contains(new Long(a.getTransactionId()))) {
67.                break;
68.            } else {
69.                // ignore actions by the index merger.
70.                if (a.getTransactionId() == MultiIndex.Action.INTERNAL_TRANS_REPL_INDEXES) {
71.                    continue;
72.                }
73.                a.execute(index);
74.            }
75.        }
76.
77.        // now we are consistent again -> flush
78.        index.flush();
79.        index.closeMultiReader();
80.    }

从上面的这段代码可以看出,如果一个事务不完整,那么这个事务的操作将会全部回滚,而事务完整的操作将会被全部重做。
通过这些操作,redolog中的信息被解析出来,并执行,那么就可以找回丢失的数据,并且过滤掉错误的数据。

总结:redolog很重要。
TO BE CONTINUE 
 

时间: 2025-01-26 19:29:13

深入浅出jcr之十 redolog 和 recovery.docx的相关文章

深入浅出jcr之十二 key-value存储系统

      在写文章方面,惰性心理无时无刻不折磨着我,文章的标题已经列在那里很长时间,可是我就是不愿意打开,不愿意把心中所想描绘出来.类似的情况可能也折磨着很多的其他同学.虽然jackrabbit是一个小众的框架,看的人和想看的人非常的少,但是其中确实包含了很多值得我们学习和研究的技术和实现,当然也有很多不足,需要我们去改进.所以我强迫自己继续写下去. 上一篇文章讲到高亮和及时搜索的问题,在文章的最后我也提出了一些问题,就是如果将高亮去掉,那么势必会带来一个问题,那就是小文本文件的存储问题,一般

深入浅出jackrabbit之十五 文档提取优化2.docx

/** *author:ahuaxuan *2009-10-22 */ 在上一篇文章中,我们讲到为什么要优化jackrabbit中的文档提取,同时也分析了进程模型和线程模型在分布式文档提取中的优劣. 在本文中,ahuaxuan将会介绍分布式文档提取的架构模型,以及它在整个非结构化数据库中的地位. 第二部分ahuaxuan将介绍几个用来提取文本的工具,然后将这些工具用在分布式文档提取中,以减轻jackrabbit的负担, 从这个角度看,本文是对上文的补充,这样从原因,到解决方案,以及所用到的技术工

深入浅出jcr之16 该死的RMI,我们需要HTTP+简单RPC协议

        从这篇文章开始,ahuaxuan不再详细描述jackrabbit中的实现原理,而是把注意力放在jackrabbit中做的不好的地方,不敢说是批判,但是有些技术上的决策错误还是值得拿出来讨论讨论的.其中一个就是jackrabbit的客户端和jackrabbit server的通信方式--RMI.围绕这个问题我们可以展开一系列的讨论. 本文分为几个部分 1 为什么要抛弃RMI 2 为什么要选择基于HTTP的RPC协议 3 展望未来 RMI这个东西原理之前很多人搞不清楚,因为sun的R

深入浅出jcr之十一 jackrabbit改进要点

在看过前面的一系列文章之后,对于jackrabbit,我们脑海里应已经有了一幅比较清晰的图.接下来我们要思考的是如何提高搜索模块的性能.其中涉及到如何正确的使用lucene,如何让搜索模块专注的做它应该做的事情. 我们先谈谈第一个话题:专注. 为什么先要谈专注呢?因为它最重要了,一个功能要能够高效的完成任务,那么它应该只做自己擅长的事情.否则就会引来别人怀疑的目光.这一点在铁岭明星本山大叔的公鸡下蛋这部作品中有着较为深刻的阐述,原文如下:"当时这个鸡---心里特别矛盾,一个公鸡,居然它要下蛋,不

深入浅出jackrabbit之十四 分布式文档提取

/** *author:ahuaxuan *2009-09-24 */ 前言: 本来针对jackrabbit这一系列的文章其实都是有顺序的,比如先讲索引的创建,然后讲索引的查询,等等,但是无奈总是有些横生的枝节,这些横生的枝节又让ahuaxuan有了一些新的想法.所以只能将这篇文章写到后面来了. 切入正题,今天这篇文章其实是对前面文本提取的一个补充.前面讲到文本提取的逻辑,逻辑有点小复杂,但是复杂的逻辑并不是这篇文章的关键,今天的话题是进程模型和线程模型,也许童鞋们会感到非常的奇怪,一个jack

[原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇)

原文:[原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇) .NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇) 前言:这个系列有段时间没有动了.主要是针对大家的反馈在修改代码.在修改的过程中,也有了一些新的体会,这里和大家分享一下,同时也发布一下业务框架的第一个版本.在本篇文章中,学习到的不是仅仅只是代码,而是设计的思想和实现这种思想的方法.在写本篇时有个感触:把一个东西彻底的讲清楚,不容易.希望大家 多提意见.而且在写本篇的时候,我个人也是很兴

[你必须知道的.NET]第十六回:深入浅出关键字---using全接触

本文将介绍以下内容: using指令的多种用法 using语句在Dispose模式中的应用 1.引言 在.NET大家庭中,有不少的关键字承担了多种角色,例如new关键字就身兼数职,除了能够创建对象,在继承体系中隐藏基类成员,还在泛型声明中约束可能用作类型参数的参数,在[第五回:深入浅出关键字---把new说透]我们对此都有详细的论述.本文,将把目光转移到另外一个身兼数职的明星关键字,这就是using关键字,在详细讨论using的多重身份的基础上来了解.NET在语言机制上的简便与深邃. 那么,us

深入浅出学习正则表达式

正则 前言: 半年前我对正则表达式产生了兴趣,在网上查找过不少资料,看过不少的教程,最后在使用一个正则表达式工具RegexBuddy时发现他的教程写的非常好,可以说是我目前见过最好的正则表达式教程.于是一直想把他翻译过来.这个愿望直到这个五一长假才得以实现,结果就有了这篇文章.关于本文的名字,使用"深入浅出"似乎已经太俗.但是通读原文以后,觉得只有用"深入浅出"才能准确的表达出该教程给我的感受,所以也就不能免俗了. 本文是Jan Goyvaerts为RegexBud

深入浅出多线程(3)-Future异步模式以及在JDK1.5Concurrent包中的实现

接深入浅出多线程(2)在多线程交互的中,经常有一个线程需要得到另个一 线程的计算结果,我们常用的是Future异步模式来加以解决. 什么是Future模式呢?Future 顾名思义,在金融行业叫期权,市场上有看跌 期权和看涨期权,你可以在现在(比如九月份)购买年底(十二月)的石油,假 如你买的是看涨期权,那么如果石油真的涨了,你也可以在十二月份依照九月份 商定的价格购买.扯远了,Future就是你可以拿到未来的结果.对于多线程,如 果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果