作者: 数据源的 TiDB 学习之路原文来源:https://tidb.net/blog/5f61f0f9
闪回(FlashBack)是一种在数据库中进行异常数据恢复的高级技术,它可以将数据库还原到之前的某个时间点,从而消除误操作、错误数据或系统故障引起的问题。最早了解闪回功能是来自 Oracle 数据库中,闪回技术是 Oracle 强大数据库备份恢复机制的一部分,主要依赖 UNDO 撤销段中的撤销数据。随着分布式技术的发展,主流的分布式数据库也基本上都支持了闪回的能力,比如 TiDB。不过由于数据库自身架构的不同,TiDB 中闪回的技术原理可能与传统数据库或其他分布式数据库有所不同。
一. 闪回的前提之 MVCC
要说 TiDB 闪回,首先得回顾一下 TiDB 的一个重要特性:MVCC。前面我们介绍闪回概念的时候提到它是将数据库还原到之前的某个时间点,那就意味着 TiDB 中要保留某个时间点的历史数据,否则就无法实现还原到这个时间点的目标。Oracle 数据库把之前一段时间的数据保存在 UNDO 中,通过 UNDO 来保存数据的历史版本,TiDB 没有 UNDO 这个东西,TiDB 的多版本是通过在数据的 Key 后面添加版本号来实现的。
TiDB 底层的数据存储 TiKV 本质上是一连串的 Key Value 键值对,在没有 MVCC 之前,可以把 TiKV 看成这样:
Key1 -> Value
Key2 -> Value
……
KeyN -> Value
复制代码
有了 MVCC 之后,TiKV 的 Key 排列是这样的:
Key1_Version3 -> Value
Key1_Version2 -> Value
Key1_Version1 -> Value
……
Key2_Version4 -> Value
Key2_Version3 -> Value
Key2_Version2 -> Value
Key2_Version1 -> Value
……
KeyN_Version2 -> Value
KeyN_Version1 -> Value
……
复制代码
对于同一个 Key 的多个版本,版本号较大的放在前面,当按照 Key+Version 查询数据时,可以直接定位到第一个大于等于这个 Key_Version 的位置。
既然数据在 TiDB 中保存了多个版本,那么数据库就一定会有垃圾回收机制,否则就会造成数据膨胀严重且性能不断下降。关于垃圾回收模块,TiDB 数据库有两个相关的参数:tikv_gc_safe_point 和 tidb_gc_life_time。tikv_gc_safe_point 表示垃圾回收已经清理到的时间点,tidb_gc_life_time 表示数据的历史版本保留时间(默认为 10 分钟)。以下输出示例表明当前环境的历史数据已经清理到 20240220-10:54:17.961 +0800 这个时间点,意味着这个时间点以前的数据被清理掉无法进行闪回了。
mysql> SELECT VARIABLE_NAME,VARIABLE_VALUE,COMMENT
-> FROM mysql.tidb
-> WHERE variable_name = "tikv_gc_safe_point";
+--------------------+-----------------------------+--------------------------------------------------------------+
| VARIABLE_NAME | VARIABLE_VALUE | COMMENT |
+--------------------+-----------------------------+--------------------------------------------------------------+
| tikv_gc_safe_point | 20240220-10:54:17.961 +0800 | All versions after safe point can be accessed. (DO NOT EDIT) |
+--------------------+-----------------------------+--------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> SHOW VARIABLES LIKE 'tidb_gc_life_time';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| tidb_gc_life_time | 10m0s |
+-------------------+-------+
1 row in set (0.00 sec)
复制代码
二. 闪回恢复
TiDB 中的闪回功能分为两类:一种是闪回恢复,即将数据恢复到某个时间点;另一种是闪回查询,即查询过去某个时间点的数据。关于闪回恢复,目前 TiDB 中主要支持以下几种方式:
此功能在 TiDB v4.0 版本中引入。在 GC life time 时间内,如果一张表被 drop 或 truncate 掉后,可以使用这个功能来恢复到 drop 或 truncate 之前的状态。下面是一个简单的示例,
mysql> create table t20(a int primary key, b int);
Query OK, 0 rows affected (0.53 sec)
mysql> insert into t20 values(1,1),(2,2),(3,3);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> select * from t20;
+---+------+
| a | b |
+---+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+---+------+
3 rows in set (0.00 sec)
mysql> drop table t20;
Query OK, 0 rows affected (1.01 sec)
mysql> select * from t20;
ERROR 1146 (42S02): Table 'test.t20' doesn't exist
mysql> flashback table t20;
Query OK, 0 rows affected (0.53 sec)
mysql> select * from t20;
+---+------+
| a | b |
+---+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+---+------+
3 rows in set (0.00 sec)
mysql> truncate table t20;
Query OK, 0 rows affected (1.02 sec)
mysql> select * from t20;
Empty set (0.01 sec)
mysql> flashback table t20;
ERROR 1050 (42S01): Table 't20' already exists
mysql> flashback table t20 to t21;
Query OK, 0 rows affected (1.03 sec)
mysql> select * from t21;
+---+------+
| a | b |
+---+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+---+------+
3 rows in set (0.00 sec)
mysql> delete from t21;
Query OK, 3 rows affected (0.01 sec)
mysql> flashback table t21;
ERROR 1105 (HY000): Can't find dropped/truncated table 't21' in GC safe point 2024-02-20 11:44:17.961 +0800 CST
复制代码
通过上述示例,我们发现两个需要注意的点:
如果表被 truncate 后进行闪回,由于原表仍然存在,需要闪回到一个不同的表名。
Delete 数据后无法使用闪回进行恢复的,针对 delete 后的恢复我们可以考虑用闪回查询的方式来恢复数据,后续会提及。
此功能在 TiDB v6.4.0 版本中引入。除了可以针对单张表进行闪回恢复,也可以针对一个 DATABASE 进行闪回恢复,它们的前提条件都是一样的,即必须要在 GC life time 时间内。以下是一个闪回数据库的示例:先创建一个数据库并创建两张测试表插入数据,之后删除数据库,最后使用 FLASHBACK 进行闪回。注意,闪回数据库也可以使用[TO newDBNname]指定恢复到一个不同的数据库名称。
mysql> create database tflashdb;
Query OK, 0 rows affected (1.02 sec)
mysql> use tflashdb;
Database changed
mysql> create table t1(a int, b int);
Query OK, 0 rows affected (0.53 sec)
mysql> insert into t1 values(1,1),(2,2),(3,3);
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> create table t2(a int, b int);
Query OK, 0 rows affected (0.52 sec)
mysql> insert into t2 values(1,1),(2,2),(3,3),(4,4);
Query OK, 4 rows affected (0.01 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> select * from t1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
3 rows in set (0.00 sec)
mysql> select * from t2;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+------+------+
4 rows in set (0.00 sec)
mysql> drop database tflashdb;
Query OK, 0 rows affected (1.02 sec)
mysql>
mysql> use tflashdb;
ERROR 1049 (42000): Unknown database 'tflashdb'
mysql> flashback database tflashdb;
Query OK, 0 rows affected (1.04 sec)
mysql> use tflashdb;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+--------------------+
| Tables_in_tflashdb |
+--------------------+
| t1 |
| t2 |
+--------------------+
2 rows in set (0.00 sec)
mysql> select * from t1;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
3 rows in set (0.01 sec)
mysql> select * from t2;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+------+------+
4 rows in set (0.00 sec)
复制代码
此功能在 TiDB v6.4.0 版本中引入。前面的 FLASHBACK TABLE 和 FLASHBACK DATABASE 目前均只能闪回到 DROP/TRUNCATE 操作前的状态,现有的 TiDB 版本(最新的 7.5)还不支持将一个 TABLE 或一个 DATABASE 闪回到某个指定时间点。不过 v6.4.0 版本中支持闪回集群到某个时间点的功能,以下示例演示分别在两个数据库中删除表和数据,然后使用闪回集群到特定的时间点后数据可以正常恢复。
mysql> use za;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select count(*) from c_t1;
+-----------+
| count(*) |
+-----------+
| 100000000 |
+-----------+
1 row in set (0.16 sec)
mysql> drop table c_t1;
Query OK, 0 rows affected (1.02 sec)
mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select count(*) from tly;
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.01 sec)
mysql> delete from tly;
Query OK, 2 rows affected (0.00 sec)
mysql> select now();
+---------------------+
| now() |
+---------------------+
| 2024-02-20 13:28:37 |
+---------------------+
1 row in set (0.00 sec)
mysql> flashback cluster to timestamp '2024-02-20 13:26:00';
Query OK, 0 rows affected (3.02 sec)
mysql> select count(*) from test.tly;
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.01 sec)
mysql> select count(*) from za.c_t1;
+-----------+
| count(*) |
+-----------+
| 100000000 |
+-----------+
1 row in set (0.03 sec)
复制代码
从这个例子中也可以看出,闪回到时间点的功能不仅可以解决 DROP/TRUNCATE 操作的问题,也可以解决误 DELETE 操作的问题。不过,如果只是一张表出现误删需要恢复到某个时间点而去恢复整个集群的话,显得动作有些大了。鉴于此,相信 TiDB 后续版本应该会增加表级别 / 库级别的闪回到时间点的功能。
三. 闪回查询
闪回查询离不开 TiDB 的另外一个概念叫“Stale Read”。在文章 聊聊 TiDB 里面如何实现读写分离 (qq.com) 中,我们提到 TiDB 支持一种叫 Follower Read 的能力读取从副本从而降低主副本的负载。Stale Read 则是一种读取历史数据版本的机制,通过 Stale Read 可以从指定时间点或时间范围内读取对应的历史数据。Stale Read 因为是随机选择一个副本读取数据,且不使用强一致性的 Follower 读,可以避免跨中心的网络延迟,降低查询访问延迟。关于 Stale Read 的详细介绍,可以参考官网文档 Stale Read 功能的使用场景 | PingCAP 文档中心。
Stale Read 可以在语句级和会话级两种方式来使用,语句级别主要使用 AS OF TIMESTAMP 语法来实现。AS OF TIMESTAMP 后可以接一个精确的时间戳,表示读取这个时间戳最新的数据;也可以接一个时间范围(使用 *TIDB_BOUNDED_STALENESS()* 函数),表示读取这个时间范围内尽可能新的数据。
闪回查询的语法也就是带 AS OF TIMESTAMP 的 SELECT 语句,以下示例演示 AS OF TIMESTAMP 分别指定时间戳和时间范围来进行闪回查询。与闪回恢复一样,要保证闪回查询成功,也必须满足查询的历史数据在 GC life time 范围内。
mysql> create table fquery(a int, b int);
Query OK, 0 rows affected (1.02 sec)
mysql> insert into fquery values(1,1);select now();
Query OK, 1 row affected (0.02 sec)
+---------------------+
| now() |
+---------------------+
| 2024-02-20 14:27:46 |
+---------------------+
1 row in set (0.00 sec)
mysql> insert into fquery values(2,2);select now();
Query OK, 1 row affected (0.01 sec)
+---------------------+
| now() |
+---------------------+
| 2024-02-20 14:27:57 |
+---------------------+
1 row in set (0.00 sec)
mysql> insert into fquery values(3,3);select now();
Query OK, 1 row affected (0.00 sec)
+---------------------+
| now() |
+---------------------+
| 2024-02-20 14:28:07 |
+---------------------+
1 row in set (0.00 sec)
mysql> select * from fquery;
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
3 rows in set (0.00 sec)
mysql> select * from fquery as of timestamp '2024-02-20 14:27:50';
+------+------+
| a | b |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.01 sec)
mysql> select * from fquery as of timestamp '2024-02-20 14:28:00';
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)
mysql> select * from fquery as of timestamp TIDB_BOUNDED_STALENESS('2024-02-20 14:27:50','2024-02-20 14:28:00');
+------+------+
| a | b |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.01 sec)
复制代码
至此,我们了解了 TiDB 闪回的基本能力,包括闪回恢复和闪回查询的能力。闪回恢复支持将表恢复到 DROP/TRUNCATE 前的状态、将数据库恢复到 DROP 前的状态、将集群恢复到指定的时间点状态。闪回查询利用 Stale Read 中的 AS OF TIMESTAMP 语法来读取一个指定时间点或时间范围的最新状态。
评论