回到过去,找回遗失的珍宝 - TiDB 的历史读功能

数据作为业务的核心,关系着整个业务的生死,所以对于数据库来说,数据的安全性是放在首位的,从宏观角度来看,安全性不仅仅在于的数据库本身足够稳定不会主动的丢失数据,有的时候更是对业务本身甚至人为失误造成损失是否有足够且便捷的应对方案,例如在游戏行业中经常遇到的反作弊(作弊玩家回档)问题,对于金融业务的审计需求等等,如果在数据库层面上提供相关机制,会让业务开发的工作量和复杂度减少很多。

传统的方案会定期备份数据,几天一次,甚至一天一次,把数据全量备份。当意外发生的时候,可以用来还原。但是用备份数据还原,代价还是非常大的,所有备份时间点后的数据都会丢失,你绝对不希望走到这一步。另外全量备份带来的存储和计算资源的额外开销,对于企业来说也是一笔不小的成本。

可是这种事情是无法完全避免的,我们所有的人都会犯错。对于一个快速迭代的业务,应用的代码不可能做到全面充分的测试,很可能因为应用逻辑的 Bug 导致数据写错,或者被恶意用户找到 bug,当你发现问题时,可以立即把应用回滚到旧版本,但是写错的数据却会一直留在数据库里。

出现这种问题的时候,你该怎么办?你只知道有些数据不对了,但是对的数据是什么,你不知道。如果能回到过去,找回之前的数据该多好。

TiDB 针对这样的需求和场景支持历史版本的读取,所以可以将错误的版本之前的数据取出来,将损失降到最低。

如何使用 TiDB 的历史读功能
使用这个功能非常简单,只需要执行一个 SET 语句:

set @@tidb_snapshot = "2016-10-10 09:30:11.123"

这个 session variable 的名字是 TiDB 里定义的 tidb_snapshot, 值是一个时间的字符串,精确到毫秒,执行了这个语句之后,之后这个客户端发出的所有读请求,读到的都是这个时间点看到的数据,这时是不能进行写操作的,因为历史是无法改变的。如果想退出历史读模式,读取最新数据,只需要再次执行一个 SET 语句:

set @@tidb_snapshot = ""

把 tidb_snapshot 设置成空字符串就可以了。

即使在那个历史时间点后,发生了 Schema 更改也没有关系,TiDB 会使用当时的 Schema 执行 SQL 请求。

TiDB 历史读功能和其他数据库的比较
这个功能 MySQL 并不支持,但是在其他的数据库里,比如 Oracle, PostgreSQL 里有类似的功能,叫做历史表(Temporial Table),是一个SQL 标准。使用的方法是需要你用特殊的建表语法,额外创建一张历史表,历史表比原表多了两个系统定义的字段,代表有效时间,这多出的两个字段是系统维护的。当原表更新数据的时候,系统会把旧版本数据插入到历史表里,当你查询历史数据时,需要用一个特殊的语法指定历史时间,得到需要的结果。

TiDB 和其他数据库的历史表功能相比,主要有以下两个优势:

1,系统默认支持

如果不是默认的行为,我们通常不会特意去建一张历史表,到真正需要用到的时候,你会发现历史表没有创建。

2,使用方便

不需要额外建一张表,不需要用特殊的语法查询。

3,全局视角,而不是以表为单位

TiDB 即使执行了 Drop Table, Drop Database 这样的操作,也可以读到旧的数据。

TiDB 的历史读功能的实现
MVCC
为了方便理解,我们这里把实现原理做了一下简化,去掉了分布式事务相关的部分,TiDB 真正的实现会复杂一些。如果想了解完整的实现细节,请持续关注 TiDB,我们会在后期逐步完善卖QQ靓号平台事务模型部分的文档帮助大家了解。

TiDB 的底层是 TiKV, TiKV 底层的存储引擎是 RocksDB, 存储的都是基本的 Key/Value Pair。在 SQL 层的一张表的一行,经过两次编码后,才得到最终的,存在 RocksDB 里的 key。

第一次编码得到的 Key,包含了 table ID 和 record ID,得到这个 key 可以定位到这一行。

第二次编码在第一次编码的 Key 的基础上,添加一个全局递增的时间戳,这个时间戳就是数据写入的时间。

所有的 Key 都带着一个全局唯一的时间戳,也就意味着,新的写入不会覆盖旧的写入,即使是删除操作,写入也只是一个删除标记,并没有真正的删数据。同一行数据的多个版本是同时存在 RocksDB 里,按照时间顺序,连续的排列在一起。

当一个读事务开始时,会从一个集群的时间分配器获取一个时间戳,这个时间点之前写入的数据,对这个事务是可见的,这个时间点之后写入的数据,对这个事务是不可见的,所以这个事务可以保证 Repeatable Read 这个隔离级别。

TiDB 在向 TiKV 发起读请求时会带上这个时间戳,在 TiKV 拿到这个时间戳后,会比较这个时间戳和这一行的多个版本,找到不大于这时间戳的最大的版本,返回给 TiDB。

以上就是 TiDB MVCC 的简化版的原理。

所以 TiDB 其实原本就是用一个历史时间来读取数据的,只不过这个历史时间是系统在事务开始时自动获取的当前时间。使用 tidb_snapshot 这个 session variable,实际上是用一个用户指定的时间取代系统自动获取的时间去读取数据。

你可能会有一个疑问,如果所有的版本都保留,数据占用的空间会不会无限膨胀?

这里就需要介绍 TiDB 的垃圾回收机制了。

TiDB 的垃圾回收
TiDB 会定期执行垃圾回收的任务,把过老的旧版本删掉,真正的从 RocksDB 里删掉,这样空间就不会无限膨胀了。

那么多久以前的老的数据会被删掉呢?这个 GC 的过期时间,是通过配置一个参数来控制的,你可以配置成十分钟,一个小时,一天或永远不回收。

所以,TiDB 的历史读功能是有限制的,只能读取到 GC 过期时间之后的数据,你可能会希望把时间设置的尽量久,但是这也是有代价的,GC 过期时间设置的越久,空间占用的会越大,读性能也会有所下降,如何配置这个时间,就要看业务的类型和需求了。

如果数据非常重要,安全是首要考虑的因素,或数据更新变动很少,建议把 GC 过期时间设置的长一点。如果数据不那么重要,或数据更新很频繁,建议把 GC 过期时间设置的短一点。

总结
TiDB 的历史读功能,把 TiDB 原生的读取机制开放出来,让用户用最简单的方式使用,我们希望这个功能,可以给 TiDB 的用户创造更多的价值。

上一篇:Chrome 55浏览器Android版推出离线下载功能 减少内存占用


下一篇:阿里云图数据库GDB助力钉钉构建百亿量级知识图谱