关于 OceanBase 4.x 中被truncate的 table 不再支持进回收站的原因

近期,OceanBase的问答社区中收到了不少用户的询问,关于OceanBase 3.x版本支持被truncate的table进入回收站的功能,为何在升级到4.x版本后不再支持了?为了解答大家的疑惑,我们将通过这篇文章来浅析 OceanBase在4.x版本中为何会出现这一"功能回调"。

背景

尽管MySQL本身并不具备回收站这样的概念和功能,但对于DBA同学来说,误操作和误删数据却是常见的问题。若MySQL拥有类似回收站的功能,那么误删的数据就可以迅速且无损地被恢复。应广大蚂蚁 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 官网目前说:通过 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 分布式下压技术》

相关推荐
OceanBase数据库官方博客21 小时前
半连接转内连接 | OceanBase SQL 查询改写
sql·oceanbase·分布式数据库
OceanBase数据库官方博客1 天前
解析在OceanBase创建分区的常见问题|OceanBase 用户问题精粹
oceanbase·分布式数据库·分区
OceanBase数据库官方博客1 天前
半连接转内连接规则的原理与代码解析 |OceanBase查询优化
sql·oceanbase·分布式数据库
IT培训中心-竺老师4 天前
OceanBase 数据库分布式与集中式 能力
数据库·分布式·oceanbase
靖顺4 天前
【OceanBase 诊断调优】—— OceanBase 数据库网络速率配置方案
网络·数据库·oceanbase
尚雷558012 天前
OceanBase 社区版 4.0 离线方式升级bp1至bp2 指南(含避坑总结)
oceanbase
五月高高12 天前
Linux部署oceanbase
linux·oceanbase
靖顺15 天前
【OceanBase 诊断调优】—— 统计信息自动收集超时导致的估行不准 SQL 选择错索引
数据库·sql·oceanbase
it界的哈士奇16 天前
Oceanbase离线集群部署
oceanbase