访问www.tomcoding.com网站,学习Oracle内部数据结构,详细文档说明,下载Oracle的exp/imp,DUL,logminer,ASM工具的源代码,学习高技术含量的内容。

DUL(Data Unloader)工具是Oracle数据库挽救数据的最后手段,你用到DUL的时候,大部分情况下,数据库已经不能启动了,甚至有些数据文件已经损坏了。它通过直接读取数据文件中的数据块,提取出有用的数据,从而实现数据恢复。
搜索网上的文章就会发现几乎都是使用手册和恢复的案例,没有人告诉你DUL是怎样实现的,本文中我们就来一步步的分析它的实现原理,如果你想自己开发一个类似的工具,这篇文章会告诉你有哪些工作要做,该怎样去做。希望你在看完这篇文章后也能尝试开发出一个自己的DUL程序。
Oracle数据库实际上是一堆数据的集合,存储在数据文件中,通过表这种逻辑结构把数据划分成不同的部分,每个表的数据有相同的结构,按照相同的格式存储在数据块中。这些数据怎样呈现给我们,就要依靠元数据来帮忙,元数据也叫数据字典,也是一些表,存储在系统表空间中,就是我们经常看到的SYSTEM表空间。
为了实现我们自己开发DUL的目的,我们首先要搞清楚的是Oracle怎样去读取和解释用户的数据。Oracle在启动时首先读取系统表空间的第一个数据文件,在这个数据文件头中有一个字段叫做rootdba,这个字段是一个系统表bootstrap$的地址,这个系统表中存储了一些基本的数据字典基础表的建表语句和表的地址。Oracle加载bootstrap$表,就把基本的数据字典表与字典数据对应起来了,通过这些数据字典表就能得到完整的数据字典。如果要扫描一个表的数据,Oracle先通过表名找到这个表的object id,然后找到这个表的段头块的地址,这个块中会有一个extent map,里面记录了这个表的段中包含了哪些extent,遍历extent中的所有块,就能得到这个extent的全部数据,遍历所有的extent就能得到这个段包含的所有数据,这样就能得到整个表的数据。如果遍历一个用户下的所有表,就能得到这个用户的数据。

数据字典

Oracle的数据字典也是由一些表组成的。其中最重要的有obj$tab$col$这三张表,obj$中指定了对象的名称(name),对象ID(obj#),对象的数据ID(dataobj#)等,当然也指定了对象的属主ID(owner#)。tab$指定了表的一些属性,最主要的是它指定了表所属的段开始的位置,在哪个数据文件(file#)中,从哪个块(block#)开始。col$指定了表的列属性,包括列的名称(name),列ID(col#),列在段中的ID(segcol#),列的类型(type#),列的最大长度(length)等等,有了col$中的信息,Oracle就能解释存储在数据块中的表的格式了。

表在数据文件中的位置

上面我们说过,tab$中有两个字段指定了表开始的位置,一个叫file#,指示表在哪个数据文件中,另一个叫block#,指示表从哪个块开始。这个开始的块叫段头块,里面包含了一个个extent地址范围,叫做extent map,extent是由连续的数据块构成的。有了这个extent map,就可以从这些块中读取数据了,这些块就是表的数据块。如果一个表非常大,段头块并不能包含所有的extent,那怎么办呢?Oracle会在这个块中指定下一个extent map的块地址,直到所有的extent map都列举完毕。
有了上面的知识,我们就可以从数据文件中读取表的数据了。在开始之前,好像还有一点问题,怎样从数据文件中读取数据字典表呢?数据字典也是表,表的段头位置又是从tab$中得到的,这时我们还没有读到tab$的数据,好像陷入死循环了。别急,前面我们提到过Oralce的启动过程,还记得那个bootstrap$系统表吗?Oracle启动时,先在内存中创建一个叫做bootstrap$的表,这个表中存储了一些建表语句,其中就包括了上面提到的obj$tab$col$,有趣的是,在每个建表语句的后面,还指示了这个表的段头块位置,那么这下就方便了,直接到这个位置找到extent map,遍历所有的extent map找到属于这个字典表的数据块,解析数据块中的内容,就可以得到数据字典的信息了。
 

数据块结构

数据块中包含了表中的数据,它也是有一定结构的,开始是块头信息,事务信息,下面是ITL,ITL大小是固定的,叫做事务槽,块中包含几个事务槽,在事务信息中指定。再后面就是数据头信息,紧接着是表目录(table directory)信息,后面是行目录(row directory),行目录指定了每行数据的位置。再后面就是行数据了,行数据是从块的底部往上存储的,所以在行目录和真正的数据之间可能会有一部分空闲的空间。数据块的结构比较复杂,好在Oracle有一个工具叫做bbed,可以打开一个数据块,它详细定义了这些数据结构,包含数据结构的各个字段,可以方便的看到数据存储的细节。

行链接

如果一行数据比较长,可能是字段比较多或者某些字段特别长,在一个块中存不下了,就会产生行链接,就是说一行数据分布在了两个或多个数据块之间,这时怎么办呢?Oracle在每行数据开始都有一个叫做fb的字段,指示数据是否链接到了下一个块,如果到了下一个块,那么就会出现一个叫做nrid的字段,用来指示后面的数据链接到了哪里,这是一个地址,代表了在哪一个块的哪个偏移量。如果下一个块还没有完全容纳这一行数据,那么会有下一个nrid,一直链接下去,直到数据行结束。如果表的结构中有LONG字段类型,就很容易产生行链接,因为LONG类型倾向于存储更长的数据。行链接会显著降低读取数据的效率,所以在设计表时应该考虑尽量避免。

LOB数据类型

在Oracle中有一个特殊的数据类型叫做大对象(Large Object)类型,就是我们常说的LOB类型,是为了替代LONG类型引入的。当数据量比较小时,它存储在表的块内,如果数据比较大,就存储在表外的一个段中,这个段叫做LOB段。LOB数据在LOB段中的位置,由一个叫做定位符的字段来指定,英文名称叫做Lob Locator,这个定位符存储在表的数据块中,这样读到LOB字段时,就可以通过定位符找到LOB数据。
LOB类型在Oracle 11g后又分为BasicFile LOB和SecureFile LOB两种,它们的存储机制发生了很大的变化,下面我们先看一下BasicFile LOB的情形。
 

BasicFile LOB

LOB的存储是相当复杂的,默认的情况下,为了方便存储,LOB列在表的数据块中,不仅存储了定位符,还存储了一些LOB数据块的地址,通过这些地址把LOB数据读出来。但是这些存储的地址个数是有限制的,这取决于表数据块中LOB信息的长度,默认情况下最多是12个,如果超出了,就要用到定位符了,定位符不能直接找到LOB段的块位置,实际上他包含LOB index的一个键值,通过这个键,在LOB索引中找到一系列的LOB块的地址,通过这些地址把LOB数据读出来。

SecureFile LOB

从11g开始,Oracle引入了一种新的LOB存储格式,叫做SecureFile LOB。它几乎把LOB index取消了,而是把LOB的块地址直接放在了LOB段的头块中,通过头块中的地址可以直接读取LOB数据。当然如果LOB数据量很大很大,头块也放不下这么多地址,那怎么办呢?Oracle在头块中设置了四个地址,分别叫做dba0,dba1,dba2,dba3。这是一个四级的内部树结构叫做itree,dba0相当于一个叶子节点,管理了很多LOB数据块地址,当dba0满了,就会出现dba1,是dba0的上级节点,它又管理了很多类似dba0的叶子,每个叶子节点块都包含了很多LOB数据块的地址,dba1满了,就会出现dba2节点,类推上去,到了dba3时,能管理的数据量已经远超过了LOB数据量的最大限制,这样所有的LOB数据都能通过这个结构遍历读取了。

Recyclebin

如果你删除一个表,从11g开始默认情况下并没有真正删除,而是把表名改变了,原来的表名存储在了一个叫回收站(recyclebin$)的表中,如果你改变了主意,还可以通过命令恢复回来,这对误删了表是一个好消息。由于它与普通表没有区别,所以通过上面的知识,我们就能恢复回来。

Truncate Table

如果一个表被截断了,那可能你就真的访问不了原来的数据了,如果现在后悔了,也只能去撞墙了。用我们前面介绍的方法能不能找回数据呢?找到表的段头块,dump出来看看,你会发现段头块中的extent map已经被清除了,这就没法通过extent map把数据遍历出来了。办法总是有的,数据不是都存储在数据文件中吗?那我们把所有数据文件中的块都扫描一遍,把跟这个表的ID一致的那些块都找出来,然后从这些块中把数据都分析出来,不就可以了吗?只是花费的时间多一些,并且要保证不能遗漏了数据文件,实践证明还是可以把数据读出来的。

Drop Table

有了上面截断表的经验,删除的表也就好处理了。段头块的改变几乎与截断表是一样的。与截断表不同的是,你要先把数据字典中删除的记录恢复出来,obj$tab$col$表中关于这个表的记录都被删除了,那么怎样恢复呢?记得前面我们提到过在每行数据前面都有一个叫做fb的字段,其实Oracle并没有把这条数据清除掉,只是在fb字段上做了一个标记,去除这个标记,这些记录就都恢复了。下面再把数据文件扫描一遍,找到属于这个表的块,就能把数据恢复了。

数据字典损坏

最严重的情况就是数据文件中有部分已经损坏了,那么就不能保证完全恢复数据了。那么首先还是要尝试读取数据字典,Oracle对基础的数据字典表存储的段头块位置都是固定的,找一个相同版本的数据库,从bootstrap$表中查找到数据字典的段头位置,或者从tab$中找到段头位置,然后尝试从这些地方导出数据字典,如果能导出数据字典,剩下的工作就跟前面一样了。
最坏的情况就是系统表空间的数据文件丢失或者严重损坏,已经无法导出数据字典了,那么这时怎么办呢?那么只有通过数据来重建数据字典了,还是把所有数据文件扫描一遍,记录段头块的位置,每个段头块会对应一个表或一个分区,这样表的段头位置就找到了,接下来要做的就是重建col$中的字段,主要是数据类型,长度等。有些数据类型的长度是固定的,比如日期类型,时间戳类型,很好猜测。数字类型也有自己的特点比较好确定,剩下的就是字符类型了,大部分猜测不到的都可以先当做字符类型处理。然后根据重建的数据字典导出一部分数据,这时就要通过人工比对,把字段类型确定清楚,然后就可以导出数据了。
 

写在最后的话

看了上面的内容,你是不是觉得开发一个DUL工具并没有那么难了?一个程序员应该做些难度比较高的开发,然后再看其他的程序才会感觉简单一些,就像你考上了大学之后才发现高中的课程其实很简单一样。如果你有兴趣挑战一下自我,也尝试写一个这样的工具吧,这个过程绝对可以帮助你提高到一个更深的层次。我当初差不多花了半年的时间开发出这样一个工具,绝大部分的时间在研究各个类型的数据块格式,尤其是SecureFile LOB的存储机制,那时网上没有任何资料参考,只能一遍遍的做实验,对照二进制和dump块的信息,猜测每个字段的含义,回想起来,感觉现在再来一遍绝对不会坚持下来。如果你不想折磨自己,可以去我的网站tomcoding.com看看整理好的现成资料还有整个工具的源代码。

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐