背景
在某些情况下,你可能会因为误操作而遇到表数据损坏或误删表的情况。为了能在事后将表数据恢复到某个特定时间点,在OceanBase尚未有表级恢复功能之前,你需要进行以下步骤:
- 利用OceanBase提供的物理恢复工具,您可以在一个新租户中完整恢复该租户至数据损坏之前的某个时间点;
- 借助OceanBase的导出工具,将恢复后租户中的表数据导出至csv文件;
- 通过OceanBase的导入工具,将导出的表数据导入至目标租户中。
是不是感觉太麻烦了?为了方便用户,针对部分表数据损坏需要恢复的这种场景,OceanBase的表级恢复功能应运而生。通过表级恢复功能,你可以直接在目标集群中告诉OceanBase,你要用哪个备份数据,将哪些表、哪些库,在哪个租户下,恢复到哪个时间点。甚至这些表或者库的备份数据也可以并不是由目标租户备份的,当然,这并不是没有限制的,你还是需要遵守一定的条件。
技术原理
OceanBase表级恢复功能整体数据流动如下图。第一步先在辅助租户中将数据恢复到指定的时间点;第二步将需要恢复的表从辅助租户导入到目标租户;最后清理辅助租户。辅助租户在这里扮演的角色类似于Oracle使用的auxiliary instance
,是OceanBase表级恢复过程中需要消耗的额外的计算存储资源。
语法说明
发起表级恢复
ALTER SYSTEM
RECOVER TABLE table_name_list
TO TENANT dest_tenant_name
FROM uri
UNTIL [TIME|SCN]=['time'|scn]
WITH 'restore_option'
REMAP TABLE remap_table_name_list
REMAP TABLEGROUP remap_tablegroup_list
REMAP TABLESPACE remap_tablespace_list
table_name_list
必选参数。需要恢复的表名,格式形如"table1,table2,..."
,多个table之间使用逗号','来分隔。table格式如"database_name.table_name"
,需要指定table的database name。database_name/table_name
的英文字母持久化到内部表中,以及比较时候基于租户、name_case_mode判断是否大小写敏感。
- 对Oracle模式而言,database_name实际上是user_name,database_name/table_name是大小写不敏感的,英文字母按照大写持久化到内部表中。
- 对MySQL模式而言,database_name就是table所属的database名字,database_name/table_name是否大小写敏感看租户的name_case_mode(一般是大小写不敏感),英文字母按照用户输入持久化到内部表中。
- database_name/table_name可能含有特殊字符,含特殊字符的database_name/table_name需要放在反引号(``)内。parser会正确地处理特殊字符,将序列化后table_name_list持久化到内部表中,使用时进行反序列化,避免解析特殊字符。
- 若要恢复一个database下的所有表,table_name处填写'*'。
- 若要恢复租户下所有的表,database_name/table_name处均填写'*'。
dest_tenant_name
必选参数。将表恢复到的目标租户,仅支持指定用户租户,必须是一个活的租户。
uri
必选参数。uri传入恢复需要使用的数据备份和归档日志的路径,但若数据备份是通过PLUS ARCHIVELOG
方式发起的,只需要填一个路径即可,否则需要分别输入数据备份和日志归档2个路径,例如:'file:///backup/archive, file:///backup/data'
,其中file:///backup/archive
是归档日志的路径,file:///backup/data
是数据备份的路径。
TIME='time' or SCN=scn
可选参数。指定表恢复的时间点,恢复到该位点为止,且包括该位点。OceanBase提供2种指定方式,分别是TIME和SCN。例如,恢复到'2023-06-01 12:00:00'
,可以用UNTIL TIME='2023-06-01 12:00:00'
,或者UNTIL SCN=1685592000000000000
。
若不指定UNTIL,默认恢复到可以恢复的最新的时间点。
restore_option
必选参数。支持指定 pool_list
、locality
、primary_zone
、kms_encrypt
, 其中 pool_list
为必选项,其余为可选项。
kms_encrypt
的取值范围如下:
true
:表示源租户为加密租户,需要恢复密钥信息。false
:表示源租户不是加密租户,不需要恢复密钥信息。如果不指定,默认为false
。
remap_table_name_list
可选参数。REMAP语句用于table name重命名,既可以不改变database,也可以重命名到其他database。若只需要remap部分table,REMAP_TABLE_NAME_LIST
只需要包含这部分table就可以了。格式如下:
// database不变,表名从Student改成Student2
REMAP TABLE School.Student:Student2
// 表名不变,database从School改成College
REMAP TABLE School.Student:College.Student
// database从School改成College,表名也从Student改成Student2
REMAP TABLE School.Student:College.Student2
// School下的所有表恢复到College库
REMAP TABLE School.`*:College.`*`
该例中,'Student'表将会被重命名成'Student2','Student2'前面不需要指定'School';'Score'表不需要重命名,因此不需要在REMAP命令中指定。源对象与重命名对象之间采用冒号连接。
remap_tablegroup_list
可选参数。tablegroup
是OceanBase独有的,若table有绑定tablegroup
,就会在目标租户创建表的时候默认使用同名的tablegroup
,若不存在,则会恢复失败,可以remap到其他tablegroup
。例如,假设School数据库下的所有表都放在同一个tablegroup tg1
下,需要恢复到目标租户的newtg1 tablegroup
下:
// tablegroup tg1下的表都恢复到目标租户的newtg1 tablegroup下
REMAP TABLEGROUP tg1:newtg1
源对象与重命名对象之间采用冒号连接。
remap_tablespace_list
可选参数。tablespace
在OceanBase中是一个逻辑单元,为了兼容Oracle而设计的,目前的作用就是为了支持数据加密的。若table有绑定tablespace
,也会在目标租户创建表的时候默认使用同名的tablespace
,若不存在,则会恢复失败,同样也可以remap到其他tablespace
。例如,假设School数据库下的所有表都放在同一个tablespace ts1
下,需要恢复到目标租户的newts1 tablespace
下:
// tablespace ts1下的表都恢复到目标租户的newts1 tablespace下
REMAP TABLESPACE ts1:newts1
源对象与重命名对象之间采用冒号连接。
取消表级恢复
发起表级恢复后,你也可以使用如下的命令来取消目标租户正在进行的表级恢复任务。需要注意的是,已经恢复的表无法取消。
ALTER SYSTEM CANCEL RECOVER TABLE dest_tenant_name
dest_tenant_name
正在执行表级恢复的目标租户名。
Related Schema
恢复说明
Schema | 是否恢复 | 说明 |
Database | 否 | 目标租户中对应的database必须要存在,否则恢复失败。如果不使用REMAP命令,例如,备份的表属于database 'HR',表级恢复也会将表放到目标租户的'HR' database下。 |
Tablespace | 否 | 目标租户中对应的tablespace必须要存在,否则恢复失败。如果不使用REMAP命令,例如,备份的表属于tablespace 'ts',表级恢复也会将表放到目标租户的'ts' tablespace下。 |
Tablegroup | 否 | 目标租户中对应的tablegroup必须要存在,否则恢复失败。如果不使用REMAP命令,例如,备份的表属于tablegroup 'tg',表级恢复也会将表放到目标租户的'tg' tablegroup下。 |
Table | 是 | 仅支持用户表,不支持系统表、临时表。 |
Partition | 是 | |
Tablet | 是 | |
Column | 是 | |
约束 | 是 | 支持恢复的约束有:NOT NULL约束UNIQUE KEY约束PRIMARY KEY约束CHECK约束若约束的名字是用户自定义的,并且在目标租户下已存在,则不恢复。 |
外键 | 否 | |
视图 | 否 | |
局部索引 | 是 | |
全局索引 | 是 | |
自增列 | 是 | |
无主键表 | 是 | |
统计信息 | 是 | |
触发器 | 否 | |
函数、存储过程、包 | 否 | |
同义词 | 否 | |
空间索引 | 是 | 坐标系不恢复,并且若坐标系在目标租户下不存在,则不恢复。 |
LOB | 是 |
使用限制
1 | 源租户和目标租户需要具有相同的compatibility_mode,例如都是MySQL模式租户,或者都是Oracle模式租户。 |
2 | MySQL模式需要保证源租户和目标租户的name case mode相同 |
3 | 分区恢复不支持 |
4 | CDC等下游无法同步表级恢复创建的表的数据 |
5 | 表名需要和系统实际存储的表名一致,例如oracle模式下创建表test时,实际创建的表是TEST,按表恢复也必须要指定TEST,否则会报表不存在错误。 |
使用示例
假设你的租户名是OBT
,数据库名是TEST
,设置的数据备份路径是'file:///data/backup/data'
,日志归档的路径是'file:///data/backup/archive'
,并且日志归档功能已经打开,也做了全量数据备份。
准备数据
# 建表
CREATE TABLE CITY (ID NUMBER PRIMARY KEY, NAME VARCHAR2(200) NOT NULL);
CREATE TABLE STUDENT (ID NUMBER, NAME VARCHAR2(100) NOT NULL, DRIVER_ID VARCHAR2(50) UNIQUE, AGE NUMBER CHECK(AGE>20), CITY_ID NUMBER, CONSTRAINT PK_STU_ID PRIMARY KEY (ID), CONSTRAINT STU_FK FOREIGN KEY(CITY_ID) REFERENCES CITY (ID)) TABLESPACE TS1;
CREATE INDEX STU_NAME_IDX ON STUDENT(NAME);
# 插入数据
INSERT INTO CITY VALUES (1, 'SHANGHAI');
INSERT INTO CITY VALUES (2, 'HANGZHOU');
INSERT INTO CITY VALUES (3, 'BEIJING');
INSERT INTO CITY VALUES (4, 'SHENZHEN');
INSERT INTO STUDENT VALUES (1, 'ALLEN', '1234567890123', 21,1);
INSERT INTO STUDENT VALUES (2, 'SAM', '2234567890123', 22,2);
INSERT INTO STUDENT VALUES (3, 'BILLY', '3234567890123',23,3);
commit;
# 收集统计信息
CALL DBMS_STATS.GATHER_TABLE_STATS('TEST', 'STUDENT', METHOD_OPT=>'FOR ALL COLUMNS SIZE 1');
# 查询当前系统时间
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') CURRENT_TIME FROM DUAL;
+---------------------+
| CURRENT_TIME |
+---------------------+
| 2023-08-21 19:47:20 |
+---------------------+
# 继续插入数据
INSERT INTO STUDENT VALUES (4, 'CARL', '4234567890123',24,4);
commit;
# 查询STUDENT表索引
SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME='STUDENT';
+-----------------------------------+
| INDEX_NAME |
+-----------------------------------+
| STUDENT_OBUNIQUE_1692618375553934 |
| STU_NAME_IDX |
| PK_STU_ID |
+-----------------------------------+
# 查询STUDENT表约束
SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE, TABLE_NAME, STATUS, INDEX_NAME FROM DBA_CONSTRAINTS WHERE TABLE_NAME='STUDENT';
+------------------------------------+-----------------+------------+---------+-----------------------------------+
| CONSTRAINT_NAME | CONSTRAINT_TYPE | TABLE_NAME | STATUS | INDEX_NAME |
+------------------------------------+-----------------+------------+---------+-----------------------------------+
| STUDENT_OBUNIQUE_1692618375553934 | U | STUDENT | ENABLED | STUDENT_OBUNIQUE_1692618375553934 |
| STU_FK | R | STUDENT | ENABLED | NULL |
| STUDENT_OBNOTNULL_1692618375553746 | C | STUDENT | ENABLED | NULL |
| STUDENT_OBCHECK_1692618375553765 | C | STUDENT | ENABLED | NULL |
| PK_STU_ID | P | STUDENT | ENABLED | PK_STU_ID |
+------------------------------------+-----------------+------------+---------+-----------------------------------+
# 查询统计信息
SELECT TABLE_NAME, NUM_ROWS FROM DBA_TABLES WHERE TABLE_NAME='STUDENT';
+------------+----------+
| TABLE_NAME | NUM_ROWS |
+------------+----------+
| STUDENT | 3 |
+------------+----------+
# 模拟事故,删除STUDENT
DROP TABLE STUDENT;
现在要将STUDENT
表恢复到'2023-08-21 19:47:20'
。
创建资源池
在sys租户下创建表级恢复过程中辅助租户需要的resource pool资源,本例中创建一个4c4g规格的资源池。
# 创建资源单元
CREATE RESOURCE UNIT RECOVER_4C4G MAX_CPU 4, MEMORY_SIZE = '4G', MAX_IOPS 1024, MIN_IOPS=1024;
# 创建资源池
CREATE RESOURCE POOL RECOVER_TMP_POOL UNIT = 'RECOVER_4C4G', UNIT_NUM = 1, ZONE_LIST = ('z1','z2','z3');
发起表级恢复
在SYS租户下发起表级恢复命令,如下的命令中包含的信息有:
- 需要恢复的表的库名是
TEST
,表名是STUDENT
- 目标租户名是
OBT
- 使用的数据备份路径是
file:///data/backup/data
,归档日志路径是file:///data/backup/archive
- 恢复到的时间点是
'2023-08-21 19:47:20'
- 辅助租户使用的resource pool是
RECOVER_TMP_POOL
,primary zone是z1 - 该表
STUDENT
恢复到目标租户OBT
的TEST
库中,并且重命名为STUDENT_RECOVER
ALTER SYSTEM RECOVER TABLE TEST.STUDENT TO TENANT OBT FROM 'file:///data/backup/data,file:///data/backup/archive' UNTIL TIME='2023-08-21 19:47:20' WITH 'pool_list=RECOVER_TMP_POOL&primary_zone=z1' REMAP TABLE TEST.STUDENT:STUDENT_RECOVER;
查看恢复进度
上述表级恢复命令执行成功后,你可以在OBT
租户中通过DBA_OB_RECOVER_TABLE_JOBS
视图查看恢复进度,STATUS
列指明恢复阶段,RESTORE_AUX_TENANT
表示正在恢复辅助租户。
SELECT * FROM DBA_OB_RECOVER_TABLE_JOBS\G
*************************** 1. row ***************************
JOB_ID: 2
INITIATOR_TENANT_ID: 1
INITIATOR_JOB_ID: 2
START_TIMESTAMP: 2023-08-21 19:54:25
END_TIMESTAMP: 1970-01-01 08:00:00
STATUS: RESTORE_AUX_TENANT
AUX_TENANT_NAME: AUX_RECOVER$1692618864733548
TARGET_TENANT_NAME: OBT
IMPORT_ALL: 0
DB_LIST: NULL
TABLE_LIST: `TEST`.`STUDENT`
RESTORE_SCN: 1692618440000000000
RESTORE_SCN_DISPLAY: 21-AUG-23 07.47.20.000000000 PM
RESTORE_OPTION: pool_list=RECOVER_TMP_POOL&primary_zone=z1
BACKUP_DEST: file:///data/backup/data,file:///data/backup/archive
BACKUP_SET_LIST: file:///data/backup/data/backup_set_1_full
BACKUP_PIECE_LIST: file:///data/backup/archive/piece_d1002r1p1
BACKUP_PASSWD: NULL
EXTERNAL_KMS_INFO: NULL
REMAP_DB_LIST: NULL
REMAP_TABLE_LIST: `TEST`.`STUDENT`:`TEST`.`STUDENT_RECOVER`
REMAP_TABLEGROUP_LIST: NULL
REMAP_TABLESPACE_LIST: NULL
RESULT: SUCCEESS
COMMENT: NULL
DESCRIPTION: NULL
查看恢复结果
等到DBA_OB_RECOVER_TABLE_JOBS
视图中没有记录时,说明任务执行结束,查询DBA_OB_RECOVER_TABLE_JOB_HISTORY
视图,可以看到任务执行结果。
# 任务执行成功
SELECT * FROM DBA_OB_RECOVER_TABLE_JOB_HISTORY\G
*************************** 1. row ***************************
JOB_ID: 2
INITIATOR_TENANT_ID: 1
INITIATOR_JOB_ID: 2
START_TIMESTAMP: 2023-08-21 19:54:25
END_TIMESTAMP: 2023-08-21 19:57:33
STATUS: COMPLETED
AUX_TENANT_NAME: AUX_RECOVER$1692618864733548
TARGET_TENANT_NAME: OBT
IMPORT_ALL: 0
DB_LIST: NULL
TABLE_LIST: `TEST`.`STUDENT`
RESTORE_SCN: 1692618440000000000
RESTORE_SCN_DISPLAY: 21-AUG-23 07.47.20.000000000 PM
RESTORE_OPTION: pool_list=RECOVER_TMP_POOL&primary_zone=z1
BACKUP_DEST: file:///data/backup/data,file:///data/backup/archive
BACKUP_SET_LIST: file:///data/backup/data/backup_set_1_full
BACKUP_PIECE_LIST: file:///data/backup/archive/piece_d1002r1p1
BACKUP_PASSWD: NULL
EXTERNAL_KMS_INFO: NULL
REMAP_DB_LIST: NULL
REMAP_TABLE_LIST: `TEST`.`STUDENT`:`TEST`.`STUDENT_RECOVER`
REMAP_TABLEGROUP_LIST: NULL
REMAP_TABLESPACE_LIST: NULL
RESULT: SUCCEESS
COMMENT: NULL
DESCRIPTION: NULL
# 3条数据被恢复
SELECT * FROM STUDENT_RECOVER;
+----+-------+---------------+------+---------+
| ID | NAME | DRIVER_ID | AGE | CITY_ID |
+----+-------+---------------+------+---------+
| 1 | ALLEN | 1234567890123 | 21 | 1 |
| 2 | SAM | 2234567890123 | 22 | 2 |
| 3 | BILLY | 3234567890123 | 23 | 3 |
+----+-------+---------------+------+---------+
# 索引被恢复
SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME='STUDENT_RECOVER';
+-------------------------------------------+
| INDEX_NAME |
+-------------------------------------------+
| STUDENT_RECOVER_OBUNIQUE_1692618993696799 |
| STU_NAME_IDX |
| PK_STU_ID |
+-------------------------------------------+
# 约束被恢复,外键没有恢复
SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE, TABLE_NAME, STATUS, INDEX_NAME FROM DBA_CONSTRAINTS WHERE TABLE_NAME='STUDENT_RECOVER';
+--------------------------------------------+-----------------+-----------------+---------+-------------------------------------------+
| CONSTRAINT_NAME | CONSTRAINT_TYPE | TABLE_NAME | STATUS | INDEX_NAME |
+--------------------------------------------+-----------------+-----------------+---------+-------------------------------------------+
| STUDENT_RECOVER_OBUNIQUE_1692618993696799 | U | STUDENT_RECOVER | ENABLED | STUDENT_RECOVER_OBUNIQUE_1692618993696799 |
| PK_STU_ID | P | STUDENT_RECOVER | ENABLED | PK_STU_ID |
| STUDENT_RECOVER_OBNOTNULL_1692618993672003 | C | STUDENT_RECOVER | ENABLED | NULL |
| STUDENT_RECOVER_OBCHECK_1692618993672009 | C | STUDENT_RECOVER | ENABLED | NULL |
+--------------------------------------------+-----------------+-----------------+---------+-------------------------------------------+
# 统计信息被恢复
SELECT TABLE_NAME, NUM_ROWS FROM DBA_TABLES WHERE TABLE_NAME='STUDENT_RECOVER';
+------------+----------+
| TABLE_NAME | NUM_ROWS |
+------------+----------+
| STUDENT | 3 |
+------------+----------+