TiDB 5.0 两阶段提交

【是否原创】是
【首发渠道】TiDB 社区

TiDB 两阶段提交

在 5.0 版本,事务提交可以分为三种方式:

  • 常规的 Percolator 2pc
  • 异步提交
  • 1pc 提交

TiDB 2PC 流程

TiDB Percolator 2pc 大体包含以下流程:

  1. Prewrite:将 buffer 的修改写入 TiKV 中
  2. 从 TSO 获取提交时间戳 commit_ts
  3. Commit Primary Key
  4. Commit 其他 Key(异步进行)

当完成第三步时候代表意味着事务被提交,就可以返回给 client 事务提交结果,第四步由后台异步进行。

异步提交流程

异步提交的目的是为了能在 Prewrite 完成阶段就可以判断事务已经完成,然后返回给 client 事务提交结果,从而缩小事务提交时间;2pc 第二阶段都异步进行。

异步提交流程:

  1. Prewrite:将 buffer 的修改写入 TiKV
  2. Commit Primary Key、Commit 其他 Key(第二阶段异步进行)

所以实现异步提交在于 prewrite 完后成如何确认事务已经提交、保证崩溃恢复后能继续提交流程以及 commit ts 的确认。

确认事务提交

在 TiDB 常规 2pc 中,secondary keys 上面会记录 primary key 信息,通过查询 primary key 判断事务是否提交来对 secondary keys 进行提交与回滚。当异步提交时候,primary key 增加个 secondary keys 列表记录,这样从 primary key 就可以找到所有的 secondary keys。如果 primary key 和 secondary keys 都 Prewrite 完成,那么就可以判断事务是提交的。

在正常 prewrite 中,如果没有报错,TiDB 就可以判断事务已经提交以及 Commit TS,异步进行第二阶段。不需要再查询所有 keys 是否提交。

计算 Commit TS

Commit TS 采用了类似 HLC 的计算方法:

  1. TiKV 本地存储了一个 max_ts,当 client(TiDB) 带着 start_ts 来访问 TiKV 时候,max_ts = max(max_ts, start_ts),max_ts 缓存了当前访问的最大 start_ts。
  2. TiDB 缓存了当前事务的 min_commit_ts(初始化为 start_ts + 1),在 prewrite 时候传给 TiKV。
  3. TiKV 在收到 prewrite 请求的时候会比较 prewrite.min_commit_ts 与 max_ts值,返回一个推高值 prewriteResp.MinCommitTs = max(prewrite.min_commit_ts, max_ts + 1)
  4. TiDB 收到 prewrite 请求结果,更新 min_commit_ts = max(min_commit_ts , prewriteResp.MinCommitTs)

最后的 min_commit_ts 将会作为事务的 Commit TS。

另外 TiKV 同时会把 prewriteResp.MinCommitTs = max(prewrite.min_commit_ts, max_ts + 1) 持久化写入,所有 keys 的最大 prewriteResp.MinCommitTs 就是 Commit TS,这是异步提交保证事务提交的关键。

满足快照事务隔离级别

因为 Commit TS 比 priwrite 时候 TiKV 的 max_ts 大,所以比先前 TiKV 上快照读事务 start_ts 大,当事务提交后,先前的 TiKV 上访问的事务不会读取到新数据,不会破坏快照隔离级别。

比如事务 T1 更新 x、y 两个 keys,T2 访问 key:y。T2 以 start_ts = 5 访问 y 值更新了 tikv 的 max_ts 为 5,那么 T1 在 prewrite y 时候至少为 max_ts + 1 = 6,那么事务 T1 至少以 Commit TS 为 6 进行提交。T2 后续 以 start_ts = 5 不会读取到 commit_ts = 6 的新值。

T1 T2
Begin(Start Ts = 1)
prewrite(x)
Begin(Start Ts = 5)
Read(y) => max_ts = 5
prewrite(y) => min_commit_ts = 6
Read(y)

满足外部一致性-实时性(real-time)

由于 TiKV 维护的 max_ts 不超过 pd 已分配的最大 ts,那么假设 pd 已经分配的最大 ts 为 100,那么 max_ts 最大为 100。

Commit_ts = max(min_commit_ts, max_ts + 1),也就是说 Commit_ts 最大值不会超过 100 + 1 = 101,。

而新事务到来,pd 分配的 ts >= 100(已分配值) + 1 = 101, 那么新事务的 start_ts >= 101,也就 >= Commit_ts 。即使相等,tikv 也是可以读取到更改后的数据。

所以对于已经提交的事务对数据的修改,后来的新事务总能读取的到。

不满足外部一致性-循序性(sequential)

事务 T1 更新 tikv1 上的 key: x,事务 T2 更新 tikv2 上的 key:y,事务 T3 读取 x、y 的值。

  1. T1 start_Ts = 101
  2. T2 start_ts = 136
  3. T3 start_ts = 198
  4. T1 更改 x,min_commit_ts = 200,以 Commit TS 为 200 提交(由于 start_ts 为 199 的其他事务访问过 tikv1)
  5. T2 更改 y, min_commit_ts = 145,以 Commit TS 为 145 提交。
  6. T1 早于 T2 提交,但是 T1:Commit_TS > T2:Commit_TS
  7. T3 以 start_ts 198 依次读取 x,y 值。虽然 x 的提交早于 y,但是 T3 读取到 y 新值,没有读取到 x 新值。

这里就违背了外部一致性的循序性,物理提交先后顺序,跟事务提交顺序不一致。

T1 T2 T3
Begin(Start Ts = 101)
Begin(Start Ts = 136)
Begin(Start Ts = 198)
prewrite(x) => min_commit_ts = 200
prewrite(y) => min_commit_ts = 145
Read(x) => T1 更改前的值
Read(y) => T2 更改后的值

满足外部一致性的异步提交

通过在 prewrite 前获取 tso 初始化 min_commit_ts 可以使得满足外部一致性,流程如下:

  1. 从 pd 获取 ts,作为 min_commit_ts 初始化值
  2. Prewrite:将 buffer 的修改写入 TiKV
  3. Commit Primary Key、Commit 其他 Key(第二阶段异步进行)

通过从 pd 获取 ts 来初始化 min_commit_ts ,使得 commit_ts 归并到 tso 排序中,使其重新有序。

满足外部一致性-循序性(sequential)

事务 T1 早于 事务 T2 提交:

T1:commt_ts = max(min_commit_ts , tikv:max_ts + 1) ,T1:commt_ts 不超过 pd 已分配最大 ts + 1.

T2:min_commit_ts(初始值) >= T1:commt_ts,那么 T2:commt_ts >= T1:commt_ts。

这里会出现相等的情况,不过不会出现能读到 T2 读不到 T1 的情况(reverse order)。

异步提交 Commit TS 不唯一

无论有没有从 pd 获取 ts,作为 min_commit_ts 初始化值,异步提交都会存在不同事务使用相同 Commit TS 的情况。

在异步提交开启情况下,binlog 是不支持的,因为 binlog 用到了 Commit TS 排序。

此外 br、cdc 要考虑好这种情况。

1PC 提交

当更改的 keys 只落在一个 region 上,并且 prewrite 只需要一次 rpc 时候,可以使用 1PC。1PC 可以看做异步提交的特殊情况。

只涉及一个 reion,不用分布式提交。

prewrite 就可以返回 Commit TS,TiKV 在处理 1PC 的 prewrite 时候,就可以判断事务是否提交,如何写入 keys 数据。

异步提交的效果

因为异步提交减少了 commit primary key 步骤就返回,理论上至少减少两次 rpc 的时间。

TiDB -> TiKV: commit primary key RPC

TiKV Raft 之间:Raft 日志复制 RPC

如果业务关闭外部一致性,还可以减少获取 Commit TSO 的 RPC。

不过获取 TSO 时间消耗极少,基本为 TiDB -> Pd 的 RTT。

所以开启外部一致性,也不会增加太多消耗。

异步提交限制

异步提交:keys 总数量、keys 总大小有限制。

优先级:1PC > 异步提交 > 常规 2PC

异步提交、1PC 可以退化为 常规 2PC.

包含很多条语句,有较长的交互逻辑的事务,事务提交的耗时占比较低,Async Commit 的提升则不会很明显。

对于 keys 较多,写入数据量较大的事务,prewrite 的耗时明显长于提交 primary key 的耗时,Async Commit 的提升也不会很明显。

参考文章

Async Commit 原理介绍 https://zhuanlan.zhihu.com/p/369002839

分布式事务中的时间戳 https://zhuanlan.zhihu.com/p/333471336

https://github.com/tikv/tikv/issues/8589

4赞