Google Percolator 分布式事务模型

最近对 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. 检查 列,如果有大于 7 的数据版本则提交失败,有则说明其他事务已经写入数据(写冲突),没有则继续处理
  3. 检查 列,如果有小于 7 的数据版本锁则提交失败,有则说明其他事务已经占用数据,没有则加锁继续处理
  4. 设置 3

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

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

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. 检查 列,如果有大于 7 的数据版本则提交失败,有则说明其他事务已经写入数据(写冲突),没有则继续处理。
  3. 检查 列,如果有小于 7 的数据版本锁则提交失败,有则说明其他事务已经占用数据,没有则加锁继续处理。
  4. 设置 9

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

Secondary Row Pre-Write

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

提交(Commit)

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

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

  1. TSO 拿到当前时间戳 commit_ts = 8
  2. 检查 列,看锁是否存在,不存在则可能已经被清除了,取消事务;存在则继续。
  3. commit_ts 为版本号,指向 bal:write 列的 start_ts 对应数据版本。也就是把 设置为 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 分布式事务模型)