TiKV事务实现浅析

本文介绍了Percolator事务在TiKV中的应用,探讨了其隔离级别、存储模型和基本步骤。Percolator事务实现了SI隔离,依赖全局时间戳来确定事务顺序。存储模型基于列族,事务分为Prewrite和Commit阶段,读写操作涉及锁管理和版本控制。Prewrite阶段选定primary和secondary rows,Commit阶段完成事务提交。在事务中,读操作需要检查锁并读取特定版本的数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Percolator事务的理论基础

Percolator的来源

Percolator事务来源于Google在设计更新网页索引的系统时提出的论文Large-scale Incremental Processing Using Distributed Transactions and Notifications中,Google用它在支持单行事务的分布式数据库Bigtable的基础上实现跨节点的分布式事务。Percolator是一种优化版的2PC,但是与 常见的2PC不同,它并没有一个单独的coodinator的角色,而是作为一个库将所有逻辑放在客户端实现,只需要下层存储支持单行事务即可。原始的Percolator事务模型中,下层的存储节点可以对于上层事务完全无感知。

 

为了确定事务的先后顺序,Percolator还要求一个全局的授时中心,用于获取全局有序的递增时间戳(比如TiDB中的pd组件)。

 

隔离级别

Percolator事务实现了SI隔离级别(TiDB中将它作为RR)。每个事务都从授时中心获取两个时间戳:startTS 和 commitTS,startTS 在事务开始时获取,commitTS在事务结束时获取,事务之间通过这两个时间戳来确定先后。例如有两个事务T1和T2,如果T1的commitTS小于T2的startTS,则认为T1发生在T2之前 ,如果两个事务的时间戳区间[startTS, commitTS]存在交叉,则两个事务是并发的。在SI隔离级别下一个事务只应该看到commitTS小于自己的startTS的事务所写入的数据。

存储模型

Percolator的存储基于Bigtable,其存储模型有列族的概念(CF),同一个列族的数据存储在一起。每个逻辑上的行分为多个列族,每个列族可以分为多个列,而其中每一列的数据以时间戳倒序排序。典型的行如下图所示:

KeyDataLockWrite
Bob6:
5:$10
6:
5:
6:data@5
5:
Joe6:
5:$2
6:
5:
6:data@5
5:

key为整个行的key,data为该行的数据。而Percolator要求额外的两个CF为:Lock和Write。Lock顾名思义表示该行的锁,而write的版本号表示写入这行数据的事务提交的时候时间戳commitTS。以Bob行为例,Key为Bob用于唯一确定该行,此时Bob没有被加锁Lock为空,在版本号为6的Write CF中有数据data@5,表示对应的数据在Data CF中版本号为5的地方。写入这行数据的事务startTS为5,commitTS为6。这里的Write CF尽管看上去额外占了一行,并不会占据额外的整行空间。

基本步骤

总体来说,TiKV 的读写事务分为两个阶段:1、Prewrite 阶段;2、Commit 阶段。

客户端会缓存本地的写操作,在客户端调用 client.Commit() 时,开始进入分布式事务 prewrite 和 commit 流程。

Prewrite 对应传统 2PC 的第一阶段

  1. 首先在所有行的写操作中选出一个作为 primary row,其他的为 secondary rows

  2. PrewritePrimary: 对 primaryRow 写入锁以及数据,锁中记录本次事务的开始时间戳。上锁前会检查:

    • 该行是否已经有别的客户端已经上锁 (Locking)
    • 是否在本次事务开始时间之后,检查versions ,是否有更新 [startTs, +Inf) 的写操作已经提交 (Conflict)

    在这两种种情况下会返回事务冲突。否则,就成功上锁。将行的内容写入 row 中,版本设置为 startTs

  3. 将 primaryRow 的锁上好了以后,进行 secondaries 的 prewrite 流程:
    • 类似 primaryRow 的上锁流程,只不过锁的内容为事务开始时间 startTs 及 primaryRow 的信息
    • 检查的事项同 primaryRow 的一致
    • 当锁成功写入后,写入 row,时间戳设置为 startTs

以上 Prewrite 流程任何一步发生错误,都会进行回滚:删除 Lock 标记 , 删除版本为 startTs 的数据。

当 Prewrite 阶段完成以后,进入 Commit 阶段,当前时间戳为 commitTs,TSO 会保证 commitTs > startTS

Commit 的流程对应 2PC 的第二阶段

  1. commit primary: 写入 write CF, 添加一个新版本,时间戳为 commitTs,内容为 startTs, 表明数据的最新版本是 startTs 对应的数据
  2. 删除 Lock 标记

值得注意的是,如果 primary row 提交失败的话,全事务回滚,回滚逻辑同 prewrite 失败的回滚逻辑。

如果 commit primary 成功,则可以异步的 commit secondaries,流程和 commit primary 一致, 失败了也无所谓。Primary row 提交的成功与否标志着整个事务是否提交成功。

事务中的读操作

  1. 检查该行是否有 Lock 标记,如果有,表示目前有其他事务正占用此行,如果这个锁已经超时则尝试清除,否则等待超时或者其他事务主动解锁。注意此时不能直接返回老版本的数据。
  2. 读取至 startTs 时该行最新的数据,找到最近的时间戳小于startTS的write CF,从其中读取版本号t,读取为于 t 版本的数据内容。

由于锁是分两级的,Primary 和 Seconary row,只要 Primary row 的锁去掉,就表示该事务已经成功提交,这样的好处是 Secondary 的 commit 是可以异步进行的,只是在异步提交进行的过程中,如果此时有读请求,可能会需要做一下锁的清理工作。因为即使 Secondary row 提交失败,也可以通过 Secondary row 中的锁,找到 Primary row,根据检查 Primary row 的 meta,确定这个事务到底是被客户端回滚还是已经成功提交。

转账示例

下面以论文中转账的一个例子来展示大体流程,以上面的Bob和Joe为例,假设Bob要转账7元给Joe。

  1. Prewrite

首先需要随机选择一行最为primaryRow ,这里选择Bob。以事务开始时间戳为版本号,写入Lock与数据

KeyDataLockWrite
Bob7:$3
6:
5:$10
7:I am Primary
6:
5:
7:
6:data@5
5:
Joe6:
5:$2
6:
5:
6:data@5
5:

从上图可以看出转账事务的startTS为7,所以写入了版本号为7的Lock与Bob的新数据,Lock中有表示自己是primaryLock的标志。随后进行secondary rows的上锁,这里只有Joe。

KeyDataLockWrite
Bob7:$3
6:
5:$10
7:I am Primary
6:
5:
7:
6:data@5
5:
Joe7:$9
6:
5:$2
7:primary@Bob
6:
5:
7:
6:data@5
5:

Joe的Lock中保存了primary的信息,用于找到这次提交的primary row Bob。

如果在prewrite的过程中检测到了冲突,则整个事务需要进行回滚。例如,在此时另一个事务的startTS为8,试图对Bob进行加锁,发现已经被startTS为7的事务加锁,则该事务会检测到冲突,事务回滚。也有可能发现在自己startTS以后,已经有事务提交了新的数据,出现了大于startTS的write,此时事务也需要回滚。

  1. Commit

    首先commit primary row,客户端通过Bigtable的单行事务,清除primary行的锁,并且以提交时间戳在write写入提交标志。

    keydatalockwrite
    Bob8:
    7:$3
    6:
    5:$10
    8:
    7:
    6:
    5:
    8:data@7
    7:
    6:data@5
    5:
    Joe7:$9
    6:
    5:$2
    7:primary@Bob
    6:
    5:
    7:
    6:data@5
    5:

    primary row的Write CF的写入是整个事务提交的标志,这个操作的完成就意味着事务已经完成提交了。
    write中写入的数据指向Bob真正存放余额的地方。完成这一步就可以向客户端返回事务commit成功了。

    接下可以异步释放secondary rows的锁。如果在commit阶段发现primary锁已经不存在(可能因为超时被其他事务清除),则提交失败,事务回滚。

    keydatalockwrite
    Bob8:
    7:$3
    6:
    5:$10
    8:
    7:
    6:
    5:
    8:data@7
    7:
    6:data@5
    5:
    Joe8:
    7:$9
    6:
    5:$2
    8:
    7:
    6:
    5:
    8data@7:
    7:
    6:data@5
    5:

    实际上,即使在执行这一步前,客户端挂了而没能处理这些行的锁也没有问题。当其他事务读取到这样的行的数据的时候,通过锁可以找出primary行,从而判断出事务的状态,如果已经提交,则可以清除锁写入提交标志。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值