浅析 4.x 版本的 truncate table 为什么不进回收站了?

2024年 5月 7日 63.9k 0

最近遇到很多用户和同学在问,3.x 版本支持被 truncate 的 table 进回收站,为什么 4.x 版本不支持了?
我们通过这篇文章和大家一起分析一下 OceanBase 为什么在 4.x 版本会出现这个 “功能回退” ?

背景

MySQL 是没有回收站的概念和功能的,但是对于 DBA 同学来说,误操作和误删数据是家常便饭,有了回收站,就可以快速且无损地找回误删的数据。应广大蚂蚁 DBA 的强烈要求,OceanBase 在 MySQL 模式下也支持了类似于 Oracle 10g 中的回收站功能。

在 3.x 版本中,当 session 级别的系统变量 ob_enable_truncate_flashback 被置为 on 时,如果进行了一个 truncate table 的误操作,可以通过 flashback 还原执行 truncate table 之前的表和数据。

例如通过执行如下 SQL 序列,就可以把被 truncate 掉的 t1 中被 truncate 之前的数据还原到另外一张叫 truncated_t1 的表中。

show variables like 'recyclebin';

set recyclebin = on; # 开启回收站的功能

show variables like 'ob_enable_truncate_flashback';

set ob_enable_truncate_flashback = on; # 开启 truncate table 进回收站的功能

create table t1(c1 int);

insert into t1 values(123);

truncate table t1;

show recyclebin;

flashback table t1 to before drop rename to truncated_t1;

最近遇到很多用户和同学在问,3.x 版本支持被 truncate 的 table 进回收站,为什么 4.x 版本不支持了?

# 下面这个系统变量在 4.x 版本已经被废弃掉了(不生效了),官网文档有误。
> set ob_enable_truncate_flashback = on;

> truncate table t1;

> show recyclebin;
Empty set (0.010 sec)

> flashback table t1 to before drop rename to truncated_t1;
ERROR 5270 (HY000): object not in RECYCLE BIN

原因分析

原因要从 OceanBase 回收站的实现方式,以及 3.x 以及 4.x 版本 truncate table 的实现方式说起。

回收站的实现方式

回收站的实现本质上是一种逻辑删除(或者叫标记删除),即把被删除的对象用内部标识进行记录,有该标识的对象对用户不可见。进入回收站的对象只修改了对象的元数据,底层数据没有任何改动。

对于回收站的实现,我们先从 show recyclebin 说起:show recyclebin 可以显示所有在回收站中对象信息,这些对象信息都保存在 __all_recyclebin 这张内部表中,每次将对象放入到回收站时,都会在该表中插入一条记录,FLASHBACK(还原) 或者 PURGE(清空)时会将信息从 __all_recyclebin 表中删除。

obclient [test]> show recyclebin;
+--------------------------------+------------------+----------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME    | TYPE     | CREATETIME                 |
+--------------------------------+------------------+----------+----------------------------+
| __recycle_$_1_1694438429313272 | liboyang_db      | DATABASE | 2023-09-11 21:20:29.314951 |
| __recycle_$_1_1694438481392392 | __idx_504469_idx | INDEX    | 2023-09-11 21:21:21.392822 |
| __recycle_$_1_1694438481414600 | t1               | TABLE    | 2023-09-11 21:21:21.415038 |
+--------------------------------+------------------+----------+----------------------------+
3 rows in set (0.011 sec)

obclient [test]> select object_name, original_name, type, gmt_create, database_id, table_id from oceanbase.__all_recyclebin;
+--------------------------------+------------------+------+----------------------------+-------------+----------+
| object_name                    | original_name    | type | gmt_create                 | database_id | table_id |
+--------------------------------+------------------+------+----------------------------+-------------+----------+
| __recycle_$_1_1694438429313272 | liboyang_db      |    4 | 2023-09-11 21:20:29.314951 |      500006 |       -1 |
| __recycle_$_1_1694438481392392 | __idx_504469_idx |    2 | 2023-09-11 21:21:21.392822 |      500001 |   504470 |
| __recycle_$_1_1694438481414600 | t1               |    1 | 2023-09-11 21:21:21.415038 |      500001 |   504469 |
+--------------------------------+------------------+------+----------------------------+-------------+----------+
3 rows in set (0.002 sec)

obclient [test]> purge database __recycle_$_1_1694438429313272;
Query OK, 0 rows affected (0.080 sec)

obclient [test]> show recyclebin;
+--------------------------------+------------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME    | TYPE  | CREATETIME                 |
+--------------------------------+------------------+-------+----------------------------+
| __recycle_$_1_1694438481392392 | __idx_504469_idx | INDEX | 2023-09-11 21:21:21.392822 |
| __recycle_$_1_1694438481414600 | t1               | TABLE | 2023-09-11 21:21:21.415038 |
+--------------------------------+------------------+-------+----------------------------+
2 rows in set (0.011 sec)

obclient [test]> flashback table t1 to before drop rename to dropped_t1;
Query OK, 0 rows affected (0.145 sec)

obclient [test]> show recyclebin;
Empty set (0.010 sec)

为了支持 drop table 进入回收站的功能,OceanBase 为每个租户都增加了一个名为 __recyclebin 的默认库,对应的 database_id 都是 201004。

obclient [test]> 
select database_id
from oceanbase.__all_database
where database_name = '__recyclebin';
+-------------+
| database_id |
+-------------+
|      201004 |
+-------------+
1 row in set (0.008 sec)

drop table 进回收站时,会修改被 drop 表的元数据信息 database_id 和 table_name,相当于把表从原来的库中移除,然后放入到这个 __recyclebin 库中。需要注意的是,drop table 时也会将表上的索引一起放到回收站里。

obclient [test]> create table t1(c1 int, index idx(c1));

obclient [test]> drop table t1;

obclient [test]> flashback table t1 to before drop rename to dropped_t1;

obclient [test]> 
select a.schema_version, a.table_id, a.database_id, a.table_name, b.ddl_stmt_str
from oceanbase.__all_table_history a, oceanbase.__all_ddl_operation b
where a.table_id = 504469 and a.table_id = b.table_id and a.schema_version = b.schema_version;
+------------------+----------+-------------+--------------------------------+--------------------------------------------------------+
| schema_version   | table_id | database_id | table_name                     | ddl_stmt_str                                           |
+------------------+----------+-------------+--------------------------------+--------------------------------------------------------+
| 1694438476085056 |   504469 |      500001 | t1                             | create table t1(c1 int, index idx(c1))                 |
| 1694438476174856 |   504469 |      500001 | t1                             |                                                        |
| 1694438481405672 |   504469 |      500001 | t1                             |                                                        |
| 1694438481414600 |   504469 |      201004 | __recycle_$_1_1694438481414600 | DROP TABLE `test`.`t1`                                 |
| 1694438664151120 |   504469 |      201004 | __recycle_$_1_1694438481414600 |                                                        |
| 1694438664161728 |   504469 |      500001 | dropped_t1                     | flashback table t1 to before drop rename to dropped_t1 |
+------------------+----------+-------------+--------------------------------+--------------------------------------------------------+
6 rows in set (0.014 sec)

   所以删除一张表进回收站整个流程如下:

      1. 修改 table 的 database_id 和 table_name,库名为 __recyclebin,表名为 __recycle 开头的特定格式。
      2. 将表放入到回收站,即在 __all_recyclebin 表中增加相应的记录。
      3. 对于表上的各个索引表,也执行上述步骤。

   flashback to before drop 是还原操作,相当于 drop to recyclebin 的逆运算,实现上和上述 drop to recyclebin 也都是正好相反的,这里不再赘述。

   purge 是清空回收站,实现是先从 __recyclebin 删除这个对象的信息,然后再从 __all_table 和 __all_table_history 中把这个对象真正删除,这里不再赘述。

   回收站中的其他对象,例如 tenant、database 等,实现上也都是类似的,这里不再赘述。

truncate table 的实现方式

3.x 版本的 truncate table

3.x 版本的 truncate table 实现上是转换成了 drop table + create table。例如执行一条 DDL truncate table t1,内部会在同一个事务中执行 drop table t1 和 create table t1。

因为 3.x 版本的 truncate table 有 drop table 这个步骤,并且 drop table 可以进回收站,所以 3.x 版本被 truncate table 之前的表和数据会跟随 drop table 这个动作进入回收站。这也就是在 3.x 版本如果我们进行了一个 truncate table 的误操作,为什么可以通过 flashback 还原执行 truncate table 之前的表和数据的原因了。

4.x 版本的 truncate table

OceanBase 在 4.x 版本新增加了一个 tablet 的概念,这是一个用户不感知的物理存储层的概念,表示可以迁移的数据块。基于这个 tablet,4.x 的 truncate table 实现上只需要修改分区对应的 tablet,不再复用 3.x 版本的 drop table + create table 的流程。即在 4.x 版本的 truncate table 中,实现变成了删除旧 tablet + 创建新 tablet,当 table 上的附属对象(例如 column、constraint、foreign key、partition、index 等)较多时,truncate table 性能可以得到巨大的优化。

因为 4.x 版本的 truncate table 已经没有 drop table 这个步骤了,所以也就不会再像 3.x 一样,把 truncate 过程中 drop 掉的这个 table 给放进回收站了。4.x 上 truncate table 前后 table 的元数据不会发生变更,只需要修改一下和存储相关的 tablet 即可。

4.x 版本为什么要调整 truncate table 的实现方式?

在 OceanBase 还是 3.x 版本时,部分客户会在业务逻辑中大量使用 truncate table 语句,但是大家都发现 OB 中 truncate table 的性能低于 Oracle。性能的消耗来自于两部分,系统表数据的更新和分区实体的创建。后者在 OB 3.x 版本上是需要创建 paxos 成员组的,步骤较多,但是在 OB 4.x 的单日志流架构下,新建分区只是在日志流内数据的操作,性能会大幅提升。而性能消耗的另外一部分是系统表数据的更新,要消除这一部分的开销,就需要改变 truncate table 的实现方式。

之前接触过一个使用 OceanBase 3.x 版本的用户,他们有上万张表,每张表都有几百个 column 和几百个 check 约束,所以每张表在 __all_column 和 __all_constraint 系统表中都会记录上百行数据。3.x 版本每次 truncate table 操作都要在 drop table 时对这些元信息执行删除操作,在 create table 时再对这些元信息执行写入操作,当 table 上的附属对象较多时,大量附属对象元信息的删除和写入会让 truncate table 耗费大量的时间。

而 OceanBase 4.x 中 truncate table 的实现不再修改表的元数据信息,所以 __all_column、__all_constraint 等系统表的数据都不会发生改变。数据的清除通过更换 tablet 来实现。__all_table 表内会增加一列 tablet_id 表示一张表对应的 tablet,truncate table 时创建一个新的空 tablet,将新 tablet_id 写入 __all_table 表替换之前的旧值,这样就完成了 truncate 表的操作,之后旧的 tablet 即可删除。对于分区表,会在 __all_part 表中记录每个分区对应的 tablet_id,truncate table 时将每个分区对应的 tablet 替换成新的空 tablet 即可。

熟悉 Oracle 的同学可以把 table_id 对应成 Oracle 的 object_id,把 tablet_id 对应成 Oracle 的data_object_id,上述 OceanBase 4.x 版本 truncate table 的实现方式就和 Oracle 的实现逻辑是类似的了。

其他

最后再说几个回收站功能中,我个人觉得比较有意思,且大家平时不容易关注到的点。

通过 original_name 来 flashback table

OceanBase 官网目前(2023.09.12)说:通过 flashback 还原回收站里的 table 时,只支持指定要恢复的数据库对象在回收站中的名称 object_name,不支持直接指定其被删除前的名称 original_name。

其实 OceanBase 也支持指定其被删除前的名称 original_name,如果回收站中的 table 有多个同名的 original_name,flashback 会还原最晚进回收站的那张表,所以 flashback original_name 的操作可以理解成是一个栈,后进先还原。该行为和 Oracle 的回收站一致。

接下来举个例子:

# 创建名字为 t1 的表,然后删除进回收站,重复三遍
obclient [test]> create table t1(c1 int);
Query OK, 0 rows affected (0.150 sec)

obclient [test]> drop table t1;
Query OK, 0 rows affected (0.448 sec)

obclient [test]> create table t1(c1 int);
Query OK, 0 rows affected (0.150 sec)

obclient [test]> drop table t1;
Query OK, 0 rows affected (0.448 sec)

obclient [test]> create table t1(c1 int);
Query OK, 0 rows affected (0.150 sec)

obclient [test]> drop table t1;
Query OK, 0 rows affected (0.448 sec)

# 回收站中会出现三张 original_name 为 t1 的表
obclient [test]> show recyclebin;
+--------------------------------+---------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME | TYPE  | CREATETIME                 |
+--------------------------------+---------------+-------+----------------------------+
| __recycle_$_1_1694489277444056 | t1            | TABLE | 2023-09-12 11:27:57.450622 |
| __recycle_$_1_1694489280576008 | t1            | TABLE | 2023-09-12 11:28:00.577040 |
| __recycle_$_1_1694489499729088 | t1            | TABLE | 2023-09-12 11:31:39.729893 |
+--------------------------------+---------------+-------+----------------------------+
3 rows in set (0.011 sec)

# 第一次 flashback 的 t1 是当前回收站中最晚(11:31)进回收站的那张 t1
obclient [test]> flashback table t1 to before drop rename to t1_1;
Query OK, 0 rows affected (0.123 sec)

obclient [test]> show recyclebin;
+--------------------------------+---------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME | TYPE  | CREATETIME                 |
+--------------------------------+---------------+-------+----------------------------+
| __recycle_$_1_1694489277444056 | t1            | TABLE | 2023-09-12 11:27:57.450622 |
| __recycle_$_1_1694489280576008 | t1            | TABLE | 2023-09-12 11:28:00.577040 |
+--------------------------------+---------------+-------+----------------------------+
2 rows in set (0.010 sec)

# 第二次 flashback 的 t1 也是当前回收站中最晚(11:28)进回收站的那张 t1
obclient [test]> flashback table t1 to before drop rename to t1_2;
Query OK, 0 rows affected (0.089 sec)

obclient [test]> show recyclebin;
+--------------------------------+---------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME | TYPE  | CREATETIME                 |
+--------------------------------+---------------+-------+----------------------------+
| __recycle_$_1_1694489277444056 | t1            | TABLE | 2023-09-12 11:27:57.450622 |
+--------------------------------+---------------+-------+----------------------------+
1 row in set (0.012 sec)

# 第三次直接通过 object_name 去 flashback 了
obclient [test]> flashback table __recycle_$_1_1694489277444056 to before drop rename to t1_3;
Query OK, 0 rows affected (0.093 sec)

obclient [test]> show recyclebin;
Empty set (0.011 sec)

通过 original_name 来 purge table

OceanBase 官网目前(2023.09.12)说:通过 purge 清空回收站中的 table 时,只支持指定要删除的表在回收站中的名称 object_name,不支持直接指定表的名称 original_name。

其实 OceanBase 也支持指定其被删除前的名称 original_name,如果回收站中的 table 有多个同名的 original_name,purge 会清空最早进回收站的那张表,所以 purge original_name 的操作可以理解成是一个队列,先进先清空。该行为和 Oracle 的回收站一致。

接下来举个例子:

# 创建名字为 t1 的表,然后删除进回收站,重复三遍
obclient [test]> create table t1(c1 int);
Query OK, 0 rows affected (0.143 sec)

obclient [test]> drop table t1;
Query OK, 0 rows affected (0.551 sec)

obclient [test]> create table t1(c1 int);
Query OK, 0 rows affected (0.146 sec)

obclient [test]> drop table t1;
Query OK, 0 rows affected (0.552 sec)

obclient [test]> create table t1(c1 int);
Query OK, 0 rows affected (0.145 sec)

obclient [test]> drop table t1;
Query OK, 0 rows affected (0.551 sec)

# 回收站中会出现三张 original_name 为 t1 的表
obclient [test]> show recyclebin;
+--------------------------------+---------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME | TYPE  | CREATETIME                 |
+--------------------------------+---------------+-------+----------------------------+
| __recycle_$_1_1694490316467736 | t1            | TABLE | 2023-09-12 11:45:16.468070 |
| __recycle_$_1_1694490326604712 | t1            | TABLE | 2023-09-12 11:45:26.605179 |
| __recycle_$_1_1694490329719016 | t1            | TABLE | 2023-09-12 11:45:29.719260 |
+--------------------------------+---------------+-------+----------------------------+
3 rows in set (0.017 sec)

# 第一次 purge 的 t1 是当前回收站中最早(11:45:16)进回收站的那张 t1
obclient [test]> purge table t1;
Query OK, 0 rows affected (0.178 sec)

obclient [test]> show recyclebin;
+--------------------------------+---------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME | TYPE  | CREATETIME                 |
+--------------------------------+---------------+-------+----------------------------+
| __recycle_$_1_1694490326604712 | t1            | TABLE | 2023-09-12 11:45:26.605179 |
| __recycle_$_1_1694490329719016 | t1            | TABLE | 2023-09-12 11:45:29.719260 |
+--------------------------------+---------------+-------+----------------------------+
2 rows in set (0.011 sec)

# 第二次 purge 的 t1 是当前回收站中最早(11:45:26)进回收站的那张 t1
obclient [test]> purge table t1;
Query OK, 0 rows affected (0.127 sec)

obclient [test]> show recyclebin;
+--------------------------------+---------------+-------+----------------------------+
| OBJECT_NAME                    | ORIGINAL_NAME | TYPE  | CREATETIME                 |
+--------------------------------+---------------+-------+----------------------------+
| __recycle_$_1_1694490329719016 | t1            | TABLE | 2023-09-12 11:45:29.719260 |
+--------------------------------+---------------+-------+----------------------------+
1 row in set (0.013 sec)

# 第三次直接通过 object_name 去 purge 了
obclient [test]> purge table __recycle_$_1_1694490329719016;
Query OK, 0 rows affected (0.134 sec)

obclient [test]> show recyclebin;
Empty set (0.010 sec)

   这篇文章暂时就先写到这里了,如果大家对 OceanBase 的回收站还有什么问题,欢迎留言或者评论,我们一起学习和探讨~

广告时间

   下面是往期的博客内容,欢迎感兴趣的同学相互交流学习:

   《OceanBase 里的 schema 是什么》

   《OceanBase 执行引擎的自适应技术》

   《OceanBase 分布式下压技术》

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论