美文网首页
Google Percolator 分布式事务模型

Google Percolator 分布式事务模型

作者: ankerjian | 来源:发表于2021-08-13 17:21 被阅读0次

    最近对 TiDB 特别感兴趣,稍微研究了一下他们应用到的 Percolator 事务模型。

    BigTable

    BigTable 是一个分布式, 多维, 映射表。本质上说,BigTable 是一个键值(key-value)映射。主要有三个维度,分别是行、列、时间戳。

    BigTable 存储映射为:(row:string, column:string, time:int64)→string

    从存储的映射时间戳维度不难看出,BigTable 是支持可以多版本控制(MVCC)的。

    BigTable支持单行的事务,可以保证一行多列的 ACID 特性。明显单行事务对于一个现代化系统来说略显不足,Percolator 借助 BigTable 实现了多行的分布式事务。

    Percolator事务流程

    Percolator事务分为两个阶段:预写(Pre-write)和提交(Commit),本质上相当于一个加强的2PC。

    需要应用 Percolator 事务的 BigTable 的表中,都需要加入下面两个列族:

    • L列: 也就是 Lock 列,需要记录该行数据的锁信息
    • W列:也就是 Write 列,需要记录该行数据 Last Committed 的数据版本,用来做版本控制。

    为什么说列族呢?首先 BigTable 一行中每一个列是允许存储多个时间版本的数据,方便实现例如:读已提交、读未提交、可重复度、序列化等事务隔离级别。

    预写(Pre-write)

    这里引用 Percolator 原文的一个例子,我们需要将 Bob 的账户中的 7 元转给 Joe 的账户中。

    初始状态

    首先我们看 bal:write 列,此时 Bob 和 Joe 最新的一个时间戳版本 6 都指向各自的 data@5,说明在 6 这个时间戳版本中: Bob 的账户余额有 10 元,Joe 的账户余额有 5 元。那么持有大于时间戳 6 的事务进行读未提交的时候,可以读到时间戳版本为 5 的 bal:data。

    初始状态

    Pre-Write

    在 Percolator 里面,首先需要把在同一个事务里面多个 Key 随机选出一个 Primary Key 和多个 Secondary Key。所在数据行分别称为 Primary Row 和 Secondary Row。

    首先进行的是 Primary Row 的 Pre-Write 操作:

    1. TSO 拿到当前时间戳 start_ts = 7
    2. 检查 <Bob bal:write> 列,如果有大于 7 的数据版本则提交失败,有则说明其他事务已经写入数据(写冲突),没有则继续处理
    3. 检查 <Bob bal:lock> 列,如果有小于 7 的数据版本锁则提交失败,有则说明其他事务已经占用数据,没有则加锁继续处理
    4. 设置 <Bob bal:data 7>3

    此时已经完成了 Primary RowPre-Write 操作。2~4 步骤需要在同一个 bigtable 事务里面进行原子操作。

    可能会有疑问为什么在此时已经把数据列 <Bob bal:data 7> 写上了?其实由于 <Bob bal:write> 列最新的数据还未写入,在其他事务看来,这属于未提交内容,其他事务可以根据事务隔离级别有选择读取 <Bob bal:data> 的时间戳版本数据。

    Primary Row Pre-Write

    那么针对多个 Secondary Row 的 Pre-Write 也与 Primary Row 类似,只是锁的记录需要指向 Primary Row 的锁。这样子实现了去中心化的锁管理,把Secondary Lock 与 Primary Lock 关联了起来。

    Secondary Row 的 Pre-Write 操作:

    1. 拿到 Primary Row 的 Pre-Write 中获得的时间戳 start_ts = 7
    2. 检查 <Joe bal:write> 列,如果有大于 7 的数据版本则提交失败,有则说明其他事务已经写入数据(写冲突),没有则继续处理。
    3. 检查 <Joe bal:lock> 列,如果有小于 7 的数据版本锁则提交失败,有则说明其他事务已经占用数据,没有则加锁继续处理。
    4. 设置 <Joebal:data 7>9

    多个 key 也是类似,在实际应用场景中,可以异步对多个 key 加锁,加快速度。

    Secondary Row Pre-Write

    自此预写(Pre-write)过程已经完成了!

    提交(Commit)

    目前为止已经把想要修改到的数据已经加好锁了,接下来需要进行 Commit 操作。

    首先进行的是 Primary Row 的 Commit 操作:

    1. TSO 拿到当前时间戳 commit_ts = 8
    2. 检查 <Bob bal:lock> 列,看锁是否存在,不存在则可能已经被清除了,取消事务;存在则继续。
    3. commit_ts 为版本号,指向 bal:write 列的 start_ts 对应数据版本。也就是把 <Bob bal:write 8> 设置为 data@7。此步骤完成后,写入的数据版本已经生效,也就是对读已提交事务可见了。
    4. 删除锁信息,让其可写。

    1~3步骤需要在一个 bigtable 事务里面进行原子操作。

    Primary Row Commit

    Secondary Row 的 Commit 操作与 Primary Row 的类似。

    Secondary Row Commit

    随便聊点

    事务隔离级别

    与传统数据库事务隔离级别(读已提交、读未提交、可重复度、可序列化)相比,Percolator 提供了快照隔离级别。

    优点:

    • 保证事务中的读操作读到对应数据版本,避免产生不可重复读的问题。
    • 保证多事务中的写操作不会更新到同一条记录。

    缺点也比较明显:

    • 写倾斜(Write)问题。
    • 乐观事务会产生写热点问题。

    锁管理

    Percolator 抛弃中心锁管理,把锁信息分散数据当中。通过区分 Primary 和 Secondary,巧妙的设置了一个标志,后续的异常处理都可以通过这个标签来进行。

    锁有可能有以下异常:

    1. Prewrite 中断,还进行 primary lock 或者写 secondary lock 到一半系统崩溃。
    2. Commit 中断,未进行 primary commit 或者 primary commit 到一半系统崩溃。

    此时就需要用到锁清理,锁清理不需要另外开任务去管理和回收。只需要在读操作的时候遇到锁的时候特殊处理即可。减轻了锁维护的成本,也简化了整个锁的管理模型。

    那是怎么处理的呢?

    每个事务开启的时候都会从 TSO 获取事务开始时间 start_ts ,通过判断某一行数据的 lock 列是否在 (0, start_ts] 范围内为两种情况:

    1. 不在;说明此锁可读:首先读取 write 列小于 start_ts 的最大的数据,然后去读 data 列。
    2. 在;说明此锁不可读,此时如果按照锁可读情况处理的话,可能会产生读未提交的问题。

    在锁不可读的情况下,也不可能无休止等待,在一定的延迟后,会进行以下操作:

    1. 遇到 primary lock 还在,可以进行锁清除。
    2. 遇到 secondary lock 还在,检查 primary lock。
      1. primary lock 还在,事务 commit 失败,回滚事务
      2. primary lock 不在,事务 commit 已经成功了,进行事务前滚(没错,就是前滚)。

    Percolator 缺点

    1. 由于依赖 TSO,会发现网络交互比较多;TiDB 团队针对退出了 Async Commit,可以减少网络交互。
    2. 乐观锁存在热点读写回滚风暴问题;TiDB 团队针对此推出了悲观事务模型。
    3. 依赖读清理锁,会有写冲突问题。

    相关文章

      网友评论

          本文标题:Google Percolator 分布式事务模型

          本文链接:https://www.haomeiwen.com/subject/xralbltx.html