ITPub博客

首页 > Linux操作系统 > Linux操作系统 > 修复数据库坏块之五

修复数据库坏块之五

原创 Linux操作系统 作者:wmlm 时间:2008-05-09 14:03:44 0 删除 编辑
===========================================================
修复数据库坏块之五
===========================================================
http://yangtingkun.itpub.net/post/468/9121

Oracle提供了DBMS_REPAIR包用来发现、标识并修改数据文件中的坏块。

任何工具都不是万能的,使用这个包的同时会带来数据丢失、表和索引返回数据不一致,完整性约束破坏等其他问题。因此当出现错误时,应当首先从物理备份或逻辑备份恢复,使用dbms_repair只是在没有备份的情况下使用的一种手段,这种方式一般都会造成数据的丢失。

dbms_repair包的工作原理比较简单,是将检查到的坏块标注出来,使随后的dml操作跳过该块,同时,dbms_repair包还提供了用于保存索引中包含的标注为坏块中的键值,以及修复freelist和segment bitmap的过程。


一、构造测试环境

首先建立一个测试用表空间,由于需要用UtralEdit打开数据文件修改部分内容来模拟错误,因此数据文件要建的小一些。

CREATE TABLESPACE TEST DATAFILE 'E:ORACLEORADATATESTTEST.DBF' SIZE 1M
2 EXTENT MANAGEMENT LOCAL AUTOALLOCATE SEGMENT SPACE MANAGEMENT MANUAL;

表空间已创建。

CREATE TABLE TEST (ID NUMBER, NAME VARCHAR2(30)) TABLESPACE TEST;

表已创建。

INSERT INTO TEST SELECT ROWNUM, OBJECT_NAME FROM DBA_OBJECTS;

COMMIT;

提交完成。

CREATE INDEX IND_TEST_ID ON TEST (ID);

索引已创建。

CREATE INDEX IND_TEST_NAME ON TEST (NAME);

索引已创建。

CONN /@TEST AS SYSDBA
已连接。
SQL> SHUTDOWN
数据库已经关闭。
已经卸载数据库。
ORACLE 例程已经关闭。
SQL> STARTUP
ORACLE 例程已经启动。

Total System Global Area 89201304 bytes
Fixed Size 453272 bytes
Variable Size 62914560 bytes
Database Buffers 25165824 bytes
Redo Buffers 667648 bytes
数据库装载完毕。
数据库已经打开。

二、模拟错误的产生

用UtralEdit打开数据文件,只要修改了数据文件中任意的一个位置,都会造成数据文件错误。但我们测试需要将错误发生位置定位在TEST表中。

CONN YANGTK/YANGTK@TEST
已连接。

SELECT SUBSTR(ROWID, 10, 6), ID, NAME FROM TEST WHERE >

SUBSTR(ROWID ID NAME
------------ ---------- ------------------------------
AAAAAG 123 ALL_REPCONFLICT

如何在数据文件中找到TEST表的数据呢?可以通过ROWID来定位的记录在数据文件中的位置。任意选择一条记录(如上面ID = 123),取得它的ROWID,我们知道,ROWID中10~15位表示这条记录所在的BLOCK是数据文件的第几个BLOCK。

A表示0,B为1,G表示6。这说明这条记录在数据文件的第六个block中。

SHOW PARAMETER DB_BLOCK_SIZE

NAME TYPE VALUE
------------------------------------ ----------- ---------------
db_block_size integer 16384

BLOCK的大小是16k。

SELECT TO_CHAR(6*16384, 'XXXXXX') FROM DUAL;

SELECT TO_CHAR(7*16384, 'XXXXXX') FROM DUAL;

用UtralEdit打开数据文件,将文件定位18000h处(以二进制方式打开,如果没有用二进制打开,可以使用CTRL+H快捷键切换)。根据上面的计算,可以得出,我们要找到记录在18000h和1C000h之间。

Number类型123在数据库存放方式为03C20218,03表示占有三位,C2表示最高位是百位,02表示最高位上是1,18表示低位上是23。

SELECT DUMP(123) FROM DUAL;

DUMP(123)
---------------------
Typ=2 Len=3: 194,2,24

SELECT TO_CHAR(194, 'XX'), TO_CHAR(2, 'XX'), TO_CHAR(24, 'XX') FROM DUAL;

上面是通过oracle的ROWID在文件中定位,这相对来说要复杂一些。下面可以使用UtralEdit的功能达到相同的目的。

ALL_REPCONFLICT。

下面用UtralEdit打开文件,使用CTRL+H方式切换到文本格式,直接查找ALL_REPCONFLICT字符串。找到后,CTRL+H切换回二进制格式。向前跳过一个长度字节(本例中为0F),就可以看到123的值03C20218,进行修改后,保存并退出。

SELECT * FROM TEST WHERE >

ID NAME
---------- ------------------------------
123 ALL_REPCONFLICT

这时候查询仍然可以得到正确结果,因为oracle使用了db_cache中的结果。为了让oracle“看”到修改,必须重起数据库。

CONN /@TEST AS SYSDBA
已连接。
SQL> SHUTDOWN
数据库已经关闭。
已经卸载数据库。
ORACLE 例程已经关闭。
SQL> STARTUP
ORACLE 例程已经启动。

Total System Global Area 89201304 bytes
Fixed Size 453272 bytes
Variable Size 62914560 bytes
Database Buffers 25165824 bytes
Redo Buffers 667648 bytes
数据库装载完毕。
数据库已经打开。
SQL> CONN YANGTK/YANGTK@TEST
已连接。

SELECT * FROM TEST WHERE >SELECT * FROM TEST WHERE >*
ERROR 位于第 1 行:
ORA-01578: ORACLE 数据块损坏(文件号7,块号6)
ORA-01110: 数据文件 7: 'E:ORACLEORADATATESTTEST.DBF'

BEGIN
2 DBMS_REPAIR.ADMIN_TABLES (
3 TABLE_NAME => 'REPAIR_TABLE',
4 TABLE_TYPE => dbms_repair.repair_table,
5 ACTION => dbms_repair.create_action,
6 TABLESPACE => 'YANGTK');
7 END;
8 /

PL/SQL 过程已成功完成。

BEGIN
2 DBMS_REPAIR.ADMIN_TABLES (
3 TABLE_NAME => 'ORPHAN_KEY_TABLE',
4 TABLE_TYPE => dbms_repair.orphan_table,
5 ACTION => dbms_repair.create_action,
6 TABLESPACE => 'YANGTK');
7 END;
8 /

PL/SQL 过程已成功完成。

REPAIR_TABLE用来记录错误检查结果,ORPHAN_KEY_TABLE用来记录表坏块中记录在索引中对应键值。

这两个表的删除可以通过下列存储过程完成

BEGIN
DBMS_REPAIR.ADMIN_TABLES (
TABLE_NAME => 'REPAIR_TABLE',
TABLE_TYPE => dbms_repair.repair_table,
ACTION => dbms_repair.drop_action);
END;
/

BEGIN
DBMS_REPAIR.ADMIN_TABLES (
TABLE_NAME => 'ORPHAN_KEY_TABLE',
TABLE_TYPE => dbms_repair.orphan_table,
ACTION => dbms_repair.drop_action);
END;
/

2.使用CHECK_OBJECT过程检测坏块。

SET SERVEROUTPUT ON
SQL> DECLARE
2 num_corrupt INT;
3 BEGIN
4 num_corrupt := 0;
5 DBMS_REPAIR.CHECK_OBJECT (
6 SCHEMA_NAME => 'YANGTK',
7 OBJECT_NAME => 'TEST',
8 REPAIR_TABLE_NAME => 'REPAIR_TABLE',
9 CORRUPT_COUNT => num_corrupt);
10 DBMS_OUTPUT.PUT_LINE('number corrupt: ' || TO_CHAR (num_corrupt));
11 END;
12 /
number corrupt: 1

PL/SQL 过程已成功完成。

SELECT OBJECT_NAME, BLOCK_ID, CORRUPT_TYPE, MARKED_CORRUPT,
2 CORRUPT_DESCRIPTION, REPAIR_DESCRIPTION
3 FROM REPAIR_TABLE;

OBJECT_NAM BLOCK_ID CORRUPT_TYPE MARKED_COR CORRUPT_DE REPAIR_DESCRIPTION
---------- ---------- ------------ ---------- ---------- ----------------------
TEST 6 6148 TRUE mark block software corrupt

这里和oracle文档上面有点出入,根据oracle文档上面介绍MARKED_CORRUPT列的值是FALSE,只有执行了FIX_CORRUPT_BLOCKS过程才会使MARKED_CORRUPT列的值变为TRUE。怀疑oracle在CHECK的同时,自动进行FIX_CORRUPT_BLOCKS的操作。

DECLARE
2 num_fix INT;
3 BEGIN
4 num_fix := 0;
5 DBMS_REPAIR.FIX_CORRUPT_BLOCKS (
6 SCHEMA_NAME => 'YANGTK',
7 OBJECT_NAME=> 'TEST',
8 OBJECT_TYPE => dbms_repair.table_object,
9 REPAIR_TABLE_NAME => 'REPAIR_TABLE',
10 FIX_COUNT=> num_fix);
11 DBMS_OUTPUT.PUT_LINE('num fix: ' || TO_CHAR(num_fix));
12 END;
13 /
num fix: 0

PL/SQL 过程已成功完成。

3.使用DUMP_ORPHAN_KEYS过程来保存坏块中的索引键值。

这时还存在着一个潜在的问题。表出现了坏块,但是索引没有损坏,通过表扫描回出现错误,但是通过索引扫描,仍然可以返回结果,这会造成数据的不一致性。

SELECT * FROM YANGTK.TEST WHERE >SELECT * FROM YANGTK.TEST WHERE >*
ERROR 位于第 1 行:
ORA-01578: ORACLE 数据块损坏(文件号7,块号6)
ORA-01110: 数据文件 7: 'E:ORACLEORADATATESTTEST.DBF'


SQL> SELECT ID FROM YANGTK.TEST WHERE >

通过使用DUMP_ORPHAN_KEYS过程来保存坏块中的索引键值,这样当执行完SKIP_CORRUPT_BLOCKS操作后,就可以重新建立索引了。

DECLARE
2 num_orphans INT;
3 BEGIN
4 num_orphans := 0;
5 DBMS_REPAIR.DUMP_ORPHAN_KEYS (
6 SCHEMA_NAME => 'YANGTK',
7 OBJECT_NAME => 'IND_TEST_ID',
8 OBJECT_TYPE => dbms_repair.index_object,
9 REPAIR_TABLE_NAME => 'REPAIR_TABLE',
10 ORPHAN_TABLE_NAME=> 'ORPHAN_KEY_TABLE',
11 KEY_COUNT => num_orphans);
12 DBMS_OUTPUT.PUT_LINE('orphan key count: ' || TO_CHAR(num_orphans));
13 END;
14 /
orphan key count: 549

PL/SQL 过程已成功完成。

DECLARE
2 num_orphans INT;
3 BEGIN
4 num_orphans := 0;
5 DBMS_REPAIR.DUMP_ORPHAN_KEYS (
6 SCHEMA_NAME => 'YANGTK',
7 OBJECT_NAME => 'IND_TEST_NAME',
8 OBJECT_TYPE => dbms_repair.index_object,
9 REPAIR_TABLE_NAME => 'REPAIR_TABLE',
10 ORPHAN_TABLE_NAME=> 'ORPHAN_KEY_TABLE',
11 KEY_COUNT => num_orphans);
12 DBMS_OUTPUT.PUT_LINE('orphan key count: ' || TO_CHAR(num_orphans));
13 END;
14 /
orphan key count: 549

PL/SQL 过程已成功完成。

4.使用REBUILD_FREELISTS过程来修改FREELISTS。

如果坏块发生在FREELIST列表中的中部,则FREELIST列表后面的块都无法访问,在这个例子中,由于是人为产生的错误,清楚错误的位置不在FREELIST中,因此可以跳过此步骤,一般情况下,无法定位坏块位置,则需要执行改过程。

BEGIN
2 DBMS_REPAIR.REBUILD_FREELISTS (
3 SCHEMA_NAME => 'YANGTK',
4 OBJECT_NAME => 'TEST',
5 OBJECT_TYPE => dbms_repair.table_object);
6 END;
7 /

PL/SQL 过程已成功完成。

5.执行SKIP_CORRUPT_BLOCKS过程,是后续DML操作跳过坏块

BEGIN
2 DBMS_REPAIR.SKIP_CORRUPT_BLOCKS (
3 SCHEMA_NAME => 'YANGTK',
4 OBJECT_NAME => 'TEST',
5 OBJECT_TYPE => dbms_repair.table_object,
6 FLAGS => dbms_repair.skip_flag);
7 END;
8 /

PL/SQL 过程已成功完成。

SELECT OWNER, TABLE_NAME, SKIP_CORRUPT FROM DBA_TABLES
2 WHERE WNER = 'YANGTK';

OWNER TABLE_NAME SKIP_COR
---------------------------- ---------------------------- --------
YANGTK TEST ENABLED
YANGTK TEST1 DISABLED
YANGTK TEST_AAA DISABLED
YANGTK TEST_PART DISABLED

6.重建索引

由于数据和索引仍然存在不一致的问题,因此必须重建索引。

SELECT * FROM YANGTK.TEST WHERE >

未选定行

SELECT ID FROM YANGTK.TEST WHERE >

ALTER INDEX YANGTK.IND_TEST_ID REBUILD;

索引已更改。

SELECT ID FROM YANGTK.TEST WHERE >

注意一点,重建索引一点要先DROP,然后再CREATE,使用REBUILD的方式,重建的数据源来自索引,仍然会导致问题的产生。

DROP INDEX YANGTK.IND_TEST_ID;

索引已丢弃。

DROP INDEX YANGTK.IND_TEST_NAME;

索引已丢弃。

CREATE INDEX YANGTK.IND_TEST_ID ON YANGTK.TEST(ID);

索引已创建。

CREATE INDEX YANGTK.IND_TEST_NAME ON YANGTK.TEST(NAME);

索引已创建。

SELECT ID FROM YANGTK.TEST WHERE >

未选定行

SELECT MIN(ID) FROM YANGTK.TEST;

----------

550

包含ID = 123的块已经别标识为坏块。现在可以看到,最小的ID是550,也就是说,这个坏块中包含了549条记录。

SELECT COUNT(*) FROM ORPHAN_KEY_TABLE;

----------

1098

继续查询ORPHAN_KEY_TABLE表,可以发现,这些记录的索引(2个)已经被保存到了ORPHAN_KEY_TABLE表中。

使用DBMS_REPAIR包的目的不仅是为了使表重新可以访问,而且,使用这个包还能在一定程度上恢复被因坏块而无法读取的数据。

由于坏块产生在表上,因此索引是可以访问,所有被索引的列的数据都可以恢复。遗憾的是,Oracle的文档并没有给出恢复的方法,我查询了METALINK和ASKTOM也没有找到相应的答案,所以,恢复的工作只能靠自己摸索进行。因此,我并不能保证我的方法一定是正确的,如果想将这种方法应用的正式环境中,请慎重考虑,本人不承担任何责任。:)

言归正传,在上面的步骤中,使用DUMP_ORPHAN_KEYS过程保存了坏块中的索引键值,下面就通过这些保存的键值来进行数据的恢复。

DESC ORPHAN_KEY_TABLE
名称 是否为空? 类型
-------------------------------------- -------- --------------
SCHEMA_NAME NOT NULL VARCHAR2(30)
INDEX_NAME NOT NULL VARCHAR2(30)
IPART_NAME VARCHAR2(30)
INDEX_ID NOT NULL NUMBER
TABLE_NAME NOT NULL VARCHAR2(30)
PART_NAME VARCHAR2(30)
TABLE_ID NOT NULL NUMBER
KEYROWID NOT NULL ROWID
KEY NOT NULL ROWID
DUMP_TIMESTAMP NOT NULL DATE

KEYROWID存放的是该索引键值对应的表中的rowid,而KEY保存的就是索引的键值。

但是查询KEY的值发现,并非像想象中一样,存放的是1、2、3……或ALL_TABLES、ACCESS$……等值,而是以ROWID方式存放的。

SELECT KEY FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_ID' AND ROWNUM = 1;

KEY
---------------------------------------------------------------
*BAAAAAACwQL+

SELECT KEY FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_NAME' AND ROWNUM = 1;

KEY
---------------------------------------------------------------
*BAAAAAAHQUNDRVNTJP4

SELECT DUMP(KEY) FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_ID' AND ROWNUM < 6;

DUMP(KEY)
----------------------------------------------------------------
Typ=208 Len=10: 2,4,0,0,0,0,2,193,2,254
Typ=208 Len=10: 2,4,0,0,0,0,2,193,3,254
Typ=208 Len=10: 2,4,0,0,0,0,2,193,4,254
Typ=208 Len=10: 2,4,0,0,0,0,2,193,5,254
Typ=208 Len=10: 2,4,0,0,0,0,2,193,6,254

这回看到希望了。还记得上面修改数据文件时123的编码吗?是不是和第一个查询中的结果很相似?

2,4,0,0,0,0前几位是不变的,最后一位254也是不变的。中间的部分就是有意义的数字了。其中第一个2表示长度是2,193表示最高位是个位,2表示最高位上的值是1,也就是说,第一个键值是数字1。

SELECT DUMP(1) FROM DUAL;

Oracle把数据在文件中的存储格式保存在ROWID字段中了。根据这个假设,我们看看字符串是不是以同样方式存储的。

SELECT DUMP(KEY) FROM ORPHAN_KEY_TABLE WHERE INDEX_NAME = 'IND_TEST_NAME' AND ROWNUM < 6;

DUMP(KEY)
----------------------------------------------------------------
Typ=208 Len=15: 2,4,0,0,0,0,7,65,67,67,69,83,83,36,254
Typ=208 Len=17: 2,4,0,0,0,0,9,65,71,71,88,77,76,73,77,80,254
Typ=208 Len=23: 2,4,0,0,0,0,15,65,71,71,88,77,76,73,78,80,85,84,84,89,80,69,254
Typ=208 Len=22: 2,4,0,0,0,0,14,65,76,76,95,65,76,76,95,84,65,66,76,69,83,254
Typ=208 Len=17: 2,4,0,0,0,0,9,65,76,76,95,65,80,80,76,89,254

SELECT CHR(65)||CHR(67)||CHR(67)||CHR(69)||CHR(83)||CHR(83)||CHR(36) FROM DUAL;

知道这个规律,就可以着手进行恢复了,首先先了一个小程序用来把ROWID类型转换为NUMBER类型或VARCHAR2类型。

CREATE OR REPLACE FUNCTION F_DUMP_FROM_ROWID
2 (
3 P_TYPE IN VARCHAR2,
4 P_DUMP_ROWID IN UROWID
5 )
6 RETURN VARCHAR2 AS
7 V_DUMP_ROWID VARCHAR2(1000);
8 V_NUMBER_STR VARCHAR2(1000);
9 V_POSITION NUMBER;
10 V_NUMBER VARCHAR2(2);
11 BEGIN
12 SELECT DUMP(P_DUMP_ROWID, 16, 7) INTO V_DUMP_ROWID FROM DUAL;
13 V_DUMP_ROWID := SUBSTR(V_DUMP_ROWID, INSTR(V_DUMP_ROWID, ',') + 1);
14 V_DUMP_ROWID := SUBSTR(V_DUMP_ROWID, 1, LENGTH(V_DUMP_ROWID) - 3);
15
16 IF P_TYPE = 'VARCHAR2' THEN
17 V_DUMP_ROWID := REPLACE(V_DUMP_ROWID , ',');
18 RETURN(UTL_RAW.CAST_TO_VARCHAR2(V_DUMP_ROWID));
19 ELSIF P_TYPE = 'NUMBER' THEN
20 V_DUMP_ROWID := V_DUMP_ROWID || ',';
21 V_ INSTR(V_DUMP_ROWID, ',');
22
23 WHILE (V_POSITION != 0) LOOP
24 V_NUMBER := SUBSTR(V_DUMP_ROWID, 1, V_POSITION - 1);
25 V_DUMP_ROWID := SUBSTR(V_DUMP_ROWID, V_POSITION + 1);
26
27 IF V_POSITION = 2 THEN
28 V_NUMBER_STR := V_NUMBER_STR || '0' || V_NUMBER;
29 ELSIF V_POSITION = 3 THEN
30 V_NUMBER_STR := V_NUMBER_STR || V_NUMBER;
31 ELSE
32 RAISE_APPLICATION_ERROR(-20002, 'DUMP ERROR CHECK THE INPUT ROWID');
33 END IF;
34
35 V_ INSTR(V_DUMP_ROWID, ',');
36 END LOOP;
37
38 V_NUMBER_STR := REPLACE(V_NUMBER_STR , ',');
39
40 RETURN(UTL_RAW.CAST_TO_NUMBER(V_NUMBER_STR));
41 ELSE
42 RAISE_APPLICATION_ERROR(-20001, 'TYPE NOT VALID OR CAN''T TRANSALTE ' || P_TYPE || ' TYPE');
43 END IF;
44 END;
45 /

函数已创建。

测试一下函数的功能。

SELECT F_DUMP_FROM_ROWID('NUMBER', KEY) FROM ORPHAN_KEY_TABLE
2 WHERE INDEX_NAME = 'IND_TEST_ID' AND ROWNUM < 6;

F_DUMP_FROM_ROWID('NUMBER',KEY)
-------------------------------------------------------------
1
2
3
4
5

SELECT F_DUMP_FROM_ROWID('VARCHAR2', KEY) FROM ORPHAN_KEY_TABLE
2 WHERE INDEX_NAME = 'IND_TEST_NAME' AND ROWNUM < 6;

F_DUMP_FROM_ROWID('VARCHAR2',KEY)
-------------------------------------------------------------
ACCESS$
AGGXMLIMP
AGGXMLINPUTTYPE
ALL_ALL_TABLES
ALL_APPLY

好了,剩下的事情就简单了。我们将ORPHAN_KEY_TABLE表中的记录转变后,重新插入到TEST表中即可。

INSERT INTO YANGTK.TEST (ID, NAME)
2 SELECT
3 F_DUMP_FROM_ROWID('NUMBER', A.KEY),
4 F_DUMP_FROM_ROWID('VARCHAR2', B.KEY)
5 FROM ORPHAN_KEY_TABLE A, ORPHAN_KEY_TABLE B
6 WHERE A.KEYROWID = B.KEYROWID
7 AND A.INDEX_NAME = 'IND_TEST_ID'
8 AND B.INDEX_NAME = 'IND_TEST_NAME';

SELECT * FROM YANGTK.TEST WHERE >

ID NAME
---------- ------------------------------
1 ACCESS$

SELECT * FROM YANGTK.TEST WHERE >

ID NAME
---------- ------------------------------
123 ALL_REPCONFLICT

COMMIT;

至此,数据恢复已经完成。

最后,需要说明几点。

我只做了NUMBER类型和VARCHAR2类型的恢复测试,其他类型的恢复还没有测试,目前我做的函数也只包含了恢复NUMBER和VARCHAR2类型的功能。而且,目前只进行了单列B数索引的恢复,组合索引、BITMAP索引等其它类型索引没有测试。另外,如果VARCHAR2(CHAR)类型中包含中文,在ZHS16GBK字符集下我测试通过,其他字符集没有测试,但估计对于一般中文字符集都不会有问题,但是单字节字符集可能会有问题。

 



引文来源  王旺的书房 | 修复数据库坏块之五


Link URL: http://wworacle.blog.163.com/blog/static/21268725200849114040675

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/271063/viewspace-263476/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2008-04-08

  • 博文量
    398
  • 访问量
    3716803