15

TiDB 的事务模块演进

 2 years ago
source link: https://www.zenlife.tk/tidb-transaction-evoluation.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2021-12-06

当前 TiDB 已经发展到 5.3 的版本了,本文简单回顾一下,从 1.0 到 5.0 这些大版本里面,事务模块都有哪些重大的变化或者改进。

Percolator

最早期的 TiDB 的事务实现是根据 Google 出的一篇 percolator 论文来的。从 1.0 时代,TiDB 就支持了分布式事务。

1.0 的时候,产品的成熟度,稳定度各方面都还不算太高。真正开始有一些重量级的用户应该从 2.0 时代开始算起。

2.0 到 2.1 应该是半年时间,其实算是大版本,只不过当时的发版本规则,准备按 2.0 2.1 2.2 2.3 这样发下去。后来调整了,所以直接奔 3.0 4.0 去了。

一直到 2.1 版本,事务模型这块都没有比较大的改动。

3.0 悲观锁

从 2.1 到 3.0 的时候,事务模块出现的第一个大的变化是,开始支持悲观事务模型。

最初 TiDB 只有乐观事务模型,乐观模型中,会先假定事务不发生冲突,直到事务提交时,才去检测冲突,如果检测到出现了冲突,就会将事务回滚。

当 TiDB 实际被一些用户使用之后,发现在业务的事务冲突比较高的场景下,乐观模型不太可行。 事务冲突之后,回滚的代价太高了,会浪费很多的写入资源。冲突之后,如果在数据库这层,帮用户实现自动事务重试,会有正确性方面的问题。如果不自动重试,把错误抛出给用户去处理,业务的使用体验又特别不友好。 而且在一些场景,冲突重试有可能再次冲突,再重试再冲突,直到重试次数上限,一直冲突下去跟死锁差不多。

悲观事务就是为了解决这个问题,它在原乐观模型的基础上改进。

在基本的 percolator 事务模型中,写入会先将数据全部缓存到 TiDB 的内存,直到事务提交时,才真正往 TiKV 写入。这也是"乐观"的由来,写入的时候可能会冲突,会导致事务提交失败。

事务提交走两阶段提交协议,第一阶段叫 prewrite 第二阶段叫 commit。prewrite 会把修改的 key 和 value 一起写下去。 悲观事务,相当于把 prewrite 加锁操作部分"提前"了。在写入的时候,会直接写入 key 和一个空的 value 占位,就相当于把 key 锁住了。由于悲观事务真的在执行过程中就把锁的 key 写下去, 这个锁就可以阻止后面的事务的写入,这样到真正的事务提交就不会再冲突了。

乐观和悲观模型是可以共存的,用户可以根据自己的使用需求去设置。在后面的版本,TiDB 更是将悲观模型设置为默认值。

4.0 大事务

解决了事务冲突的问题之后,接下来的一个改进是单个事务的大小。

TiDB 最初没法支持单个事务过大,主要的原因是在 prewrite 阶段的写入会阻塞读操作。如果事务比较大,写入的耗时会比较长,写阻塞读是不可以接受的。

所以为了支持大事务,实际上对事务协议这一层做了比较大的调整。在事务的协议引入了一个 min commit ts 的概念,当读操作读到正在提交中的写事务,它会设置写事务的 min commit ts,从而调整了两个事务的先后顺序。

读操作会推写操作的 min commit ts,然后写操作提交时,保证最终的 commit ts 大于 min commit ts。效果相当于让写事务发生成读事务之后。既然写在后,读在前,读操作是可以不用管写操作是否完成的状态,读上一个 MVCC 版本数据的。

至此,在单机内存足够的情况下,TiDB 可以支持到 10G 大事务,并且事务协议的改动让读不再等待写事务,在发生读写冲突的场景,读的延迟会明显降低。

4.0 到 5.0 的大版本时间挺长的,中间有一些优化工作,比如优化大事务的内存占用是在 4.0 发版之后继续推进的。还有悲观锁的 pipeline 优化,是在 4.0 版本里面推出的。这个优化主要是让悲观锁可以不用走完 raft 同步多副本过程,直接返回,因为悲观锁的正确性保证并不需要这里 100% 成功,所以可以走 pipeline 优化。

5.0 的 Async Commit 和 1PC

5.0 版本事务方面,引入的最令人兴奋的特性是 async commit。

async commit 把两阶段提交的第二个阶段,直接变成了异步提交,也就是说,只要执行完第一个阶段,就可以成功返回到客户,这几乎将事务的写入延迟直接减半了!

实现 async commit 有两个难点需要解决,哦,说白了其实是一个问题:如何保证第一阶段 prewrite 操作成功了,就算成功了?由这个问题引出的两个点,prewrite 阶段如果遇到了异常,比如机器宕机,如何处理事务的最终状态,这是第一个。事务的 commit ts 怎么取,这是第二个问题。

为了解决 prewrite 异常之后的状态,协议层的改动是,在 primary key 的 value 里面,直接把所有的 secondary key 记录下来,这样如果事务状态不确定的时候,可以通过 secondary key 的状态查询到 primary key,再通过 primary key 知道所有 secondary keys,接着查所有 secondary keys 状态就可以得出事务状态最终是成功或失败。

由于 commit 操作异步化了,prewrite 完成直接返回给客户,那么 prewrite 的时候就需要知道最终的 commit ts。怎么取 commit ts 这一层的协议就需要修改。这个 ts 需要是一个大于所有已提交的事务的 ts,而又要小于所有尚未提交的事务的 ts。最终取的是一个计算得到的值,具体细节就比较复杂了。

5.0 的 TiDB 还有一个相对小一点的优化:1PC(一阶段提交)。一阶段提交有一定的限制条件,它要求事务所有的修改只涉及到一个 region,这种情况下不涉及多个 region 其实不需要分布式的事务,只要单个 region 写成功就成功了。因此也不需要两阶段提交去处理,有些 region 成功有些 region 可能失败这样的分布式难题。

因为 async commit 已经是只需要完成第一个阶段就可以返回客户的,1PC 相对 async commit 并没有实质的延迟上面的优化,并且通用性也不如 async commit。1PC 的好处是它直接省掉了第二个阶段,可以降低一点写入量。

5.0 之后的发版模型会变成 2 3 个月一个小版本,5.1 5.2 5.3 这样子发版,这几个小版本目前还没有出现事务协议方面的大的改动。

以上就是 TiDB 一路过来,事务模块的演进过程。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK