背景
在大多数业务场景中,数据库主要负责提供对当前数据的查询能力。然而,对于一些特定业务场景,如游戏运营和金融交易等,查询历史数据的能力尤为重要。例如,在游戏场景中,通过查询历史数据来追踪和定位账号装备的异常变动。在数据恢复、审计、分析等场景,闪回查询可以帮助用户了解数据的历史变化,以便进行决策支持或问题诊断。此外,在日常的运维过程中,如果发生误操作,闪回查询提供了更为灵活的时间点选择。与依赖历史备份进行日志回放相比,它能够更快实现数据恢复。
GaiaDB的闪回查询通过一条简单的SQL语句,即可访问历史数据。无论是需要纠正刚刚发生的错误操作,还是进行长期趋势的数据分析,GaiaDB都能提供快速、准确的数据查询服务。这不仅极大地减轻了业务流程中对数据查询的需求复杂度,也提高了数据管理的效率。本文深入探讨了GaiaDB闪回查询技术,并提供了示例演示。
使用方法
GaiaDB引入了新的查询语法来支持闪回查询,只需要在select语句后添加AS OF TIMESTAMP + 时间戳
便可以查询某一历史版本的数据。语法如下:
SELECT column_name_list FROM table_name AS OF TIMESTAMP time_expr alias WHERE...;
技术方案
1、基于undo log的版本链
GaiaDB的闪回查询功能利用MVCC(多版本并发控制)特性实现。在原生的InnoDB引擎中,MVCC特性的设计目的是解决只读事务和写事务之间的冲突。根据数据库不同的隔离级别的限制,写事务的修改对于读事务来说有着不同的可见性。为了实现这样的可见性需求,InnoDB中每行数据都维护了修改自己的undo日志,这些undo记录按照从新到旧的顺序组成了本行数据的历史版本链,如下图所示。在查询时,可以通过这个版本链向前追溯历史版本,通过记录的事务号和本查询的ReadView进行可见性判断,从而查询出当前读事务所能读到的正确数据。出于节省空间的考虑,历史版本链不会无限增长,若某个历史版本对于所有事务都不再需要,那么便可以将该历史版本清理掉。
2、GaiaDB对于MVCC读取的改造
如果读事务持有某一历史时刻的ReadView,并且此时刻的历史版本尚未被清理,那么就能很自然地获得需要的历史数据版本。因此闪回查询的实现,首先是保存历史的ReadView及对应的数据版本。GaiaDB新增了一个系统表History ReadViews,表中的每行数据为一个二元组,分别是时间戳和该时间戳对应的ReadView经序列化后的字符串,如下图所示。在开启闪回查询之后,后台线程会以1秒为周期,从事务系统获取当前的ReadView,经序列化后与当前的时间戳组成二元组,插入到系统表中。在清理历史版本链时,限制条件也发生了变化。History ReadViews表中最早时间戳对应的ReadView决定了undo记录是否可以被清理。如下图示例中,系统表中最早的时间戳为"2024-04-04 16:16:16",那么以此时刻的ReadView为标准,已经提交事务所相关的undo记录才能够被清理。
在需要查询某一历史时刻的版本时,只需从系统表中获取到该时刻对应的ReadView作为本次查询的ReadView,即可查询到特定时刻的历史数据。以下图为例,若某行数据在T0时刻值为“A”,在T1时刻被修改为了“B”,又在T2时刻被修改为了“C”。此时进行普通的查询,会首先在事务系统中获取当前时刻的ReadView,由于之前的修改都已提交,所以该ReadView显示能够读取到最新版本的数据,然后从B+树上查询得到数据值为“C”。
若使用闪回查询,查询T0时刻的数据,那么首先会从History ReadViews系统表中查询T0时刻的ReadView。由于在T0时刻,后续的修改还未发生,所以该ReadView显示只有最早的历史版本1对于本次查询来说是可见的。使用这个ReadView再去数据行的历史版本链上遍历,首先发现最新版本"C"是不可见的,继续向前追溯,直至发现可见的历史版本1,返回结果为“A”。
3、IO暴涨问题和解决方案
在InnoDB引擎中,二级索引不记录事务号,所以在MVCC中二级索引的可见性判断,需要回查主键判断。又由于每个undo记录可能分布在不同的数据页上,所以每个undo记录的访问都可能触发IO。开启闪回查询时,历史版本的清理被推迟,历史版本链很长,这会导致InnoDB的性能急剧下降,极端情况会导致实例保护性自杀。针对这个问题,GaiaDB将历史版本清理的过程拆分为两个阶段。第一个阶段为二级索引的清理,这个过程在事务提交之后即可进行。经过这一阶段,所有对应二级索引上的delete mark被删除,只保留其在主键索引上的位置。第二个阶段为主键索引的清理,这一阶段的进度由系统表History ReadViews中最老的ReadView限制,逐秒推进。经过这一阶段,对应的主键索引上的delete mark也被清除,完成最终的清理任务。这种方案舍弃了提供二级索引数据闪回查询的能力,但是很好地解决了IO暴涨的问题。
具体示例
下面用一个具体的使用场景说明如何用GaiaDB轻松地获取历史数据。
1、使用如下语句创建表结构并插入初始数据。
create table products (prod_id bigint(10) primary key NOT NULL,prod_name varchar(20) NOT NULL,cust_id bigint(10) NULL,createtime datetime NOT NULL DEFAULT NOW());
INSERT INTO products(prod_id,prod_name,cust_id,createtime) values (101,'Book',1,NOW()),(102,'Apple',1,NOW()),(103,'Beef',2,NOW()),(104,'Bread',3,NOW()),(105,'Cheese',4,NOW());
2、记录下当前的时间戳,2024-04-12 16:42:41。
SELECT NOW();
3、对数据进行修改,修改prod_name
为Book和Apple的prod_id
,由101和102分别修改为110和119
UPDATE products SET prod_id = 110, createtime = NOW() WHERE prod_name = "Book";
UPDATE products SET prod_id = 119, createtime = NOW() WHERE prod_name = "Apple";
4、使用普通的查询语句可以查询到更新后的数据。
SELECT * FROM products;
+---------+-----------+---------+---------------------+
| prod_id | prod_name | cust_id | createtime |
+---------+-----------+---------+---------------------+
| 103 | Beef | 2 | 2024-04-12 16:42:35 |
| 104 | Bread | 3 | 2024-04-12 16:42:35 |
| 105 | Cheese | 4 | 2024-04-12 16:42:35 |
| 110 | Book | 1 | 2024-04-12 16:42:56 |
| 119 | Apple | 1 | 2024-04-12 16:42:57 |
+---------+-----------+---------+---------------------+
5 rows in set (0.00 sec)
5、使用闪回查询的as of timestamp语法查询查看products
表中2024-04-12 16:42:41
这个历史时间点的数据。
SELECT * FROM products AS of TIMESTAMP '2024-04-12 16:42:41';
+---------+-----------+---------+---------------------+
| prod_id | prod_name | cust_id | createtime |
+---------+-----------+---------+---------------------+
| 101 | Book | 1 | 2024-04-12 16:42:35 |
| 102 | Apple | 1 | 2024-04-12 16:42:35 |
| 103 | Beef | 2 | 2024-04-12 16:42:35 |
| 104 | Bread | 3 | 2024-04-12 16:42:35 |
| 105 | Cheese | 4 | 2024-04-12 16:42:35 |
+---------+-----------+---------+---------------------+
可以看到prod_name
为Book和Apple的prod_id
为未修改前的101和102。