黄东旭解析 TiDB 的核心优势
1839
2024-02-20
本篇文章着重更详细的介绍 Prewrite 接口内部逻辑,看一下对于各种各样的异常场景是如何处理的。
下面的场景样例均以下面的例子为基础:
Let’s see the example from the paper of Percolator. Assume we are writing two rows in a single transaction. At first, the data looks like this:
This table shows Bob and Joe’s balance. Now Bob wants to transfer his $7 to Joe’s account.
Get the
start_tsof the transaction. In our example, it’s7.
为了分布式事务的正确性,在执行 Prewrite 前,需要对 Prewrite 涉及的 KEY 都要进行如下检查:
检查在 lock_cf 中没有记录,也就是没有锁
检查在 write_cf 中没有大于等于当前事务 start_ts 的记录
例如下面的例子 (start_ts=7) 就可以通过前置检查:

可以看到存在两个 KEY,一个是 Bob,一个是 Joe。两个 KEY 的 lock_cf 都是空的,同时 write_cf 的最新记录是 6,小于 start_ts(7)
例如下面的例子 (start_ts=7) ,前置检查就会失败,因为 Bob 的 lock_cf 存在一个 ts 为 9 的 primary lock:

例如下面的例子 (start_ts=7) ,前置检查就会失败,因为 Bob 的 write_cf 存在一个 commit_ts=9 的记录:

前置检查通过后,我们开始进行真正的 Prewrite 操作。
作为2PC 的第一阶段,预提交。目的是将事务涉及的多个 KEY-VALUE 写入 default_cf,同时将在 lock_cf 上加锁
将 KEY-VALUE 写入 default_cf
将 lock 信息写入 lock_cf 上加锁
Prewrite 操作前,存储的事务状态为:

对 Bob 和 Joe 进行 Prewrite 操作后,存储的事务状态为:

值得注意的是,tidb 指定 Bob 是 primary key,Bob 写入的 lock 是 primary lock。指定 Joe 是 secondary key,Joe 写入的 lock 是 secondary lock。
通过 Joe 的 secondary lock 我们可以定位到其 primary key 是 Bob。Bob 的当前状态代表了整个事务 t0 当前的状态
上面所述都是比较乐观的场景,但是现实上可能会遇到各种并发问题或者网络问题,导致 Prewrite 的前置检查失败。
假如只有一个事务 t
事务 t 刚刚执行了 Prewrite 、或者Prewrite超时 后,可能由于网络原因又对同一个事务 t 调用 Prewrite,会返回 OK (1.1)
事务 t 已经 Commit Primary Key、Commit Secondary Key 完毕了,由于网络原因又对同一个事务 t 调用 Prewrite,会返回 OK (2.1)
事务 t 已经 Rollback 完毕了,由于网络原因又对同一个事务 t 调用 Prewrite,会返回 WriteConflict (2.2)
假如有事务 t、t1 ,他们更新的 KEY 相同,假如事务 t 完毕了,事务 t1 才启动,
事务 t1 执行了 Prewrite /Commit Primary Key/Commit Secondary Key/Rollback后,由于网络原因又对已经完毕的事务 t 调用 Prewrite,
假如事务 t 已经 Commit,会返回 OK (1.2)(2.1)
假如事务 t 已经 Rollback,会返回 WriteConflict (1.3)(2.2)
假如有事务 t、t1 ,他们更新的 KEY 相同,事务 t 先启动后,事务 t1 后启动 (t.start_ts < t1.start_ts)
事务 t1 已经执行了 Prewrite,未来得及 Commit,这时候 t 才进行 Prewrite,会返回 KeyIsLocked (1.4)
事务 t1 已经执行了 Prewrite 后 Down 了,这时候 t 才进行 Prewrite,会返回 KeyIsLocked (1.4)
事务 t1 已经执行了 Commit Primary Key、Commit Secondary Key,这时候 t 才进行 Prewrite,会返回 WriteConflict (2.3)
事务 t1 已经执行了 Rollback,这时候 t 才进行 Prewrite,会返回 WriteConflict (2.3)
下面将会讲解各个异常检查的细节逻辑以及其相应的样例场景。
Prewrite 的 LOCK 前置检查失败的情况下,例如下图中 Bob 这个 KEY 就存在着一个 primary lock:

并不是直接报错,而是会进行进一步的检查。
如果发现其中一个 Key 已经被加锁,判断这个 lock 是不是本事务的 (lock.ts=t.start_ts)
1.1 如果是的话,那么就是接口重复调用,保持幂等,返回 OK (场景一)
否则的话,说明这个 lock 不是本事务的,需要根据 t.start_ts 继续搜索 write_cf 中的 write 记录
None 记录指的是:
符合条件 ( record.start_ts != t.start_ts && record.commit_ts != t.start_ts) 的 write 记录 (场景五、场景六、场景七)
或者,符合条件 (record.commit_ts = t.start_ts && has_overlapped_rollback = false ) 的 write 记录 (场景八)
Rollback 记录指的是:
符合条件 ( record.start_ts = t.start_ts && record.type = Rollback) 的 write 记录 (场景三)
或者,符合条件 (record.commit_ts = t.start_ts && has_overlapped_rollback = true ) 的 write 记录 (场景四)
Commit 记录是指:
( record.start_ts = t.start_ts && record.type != Rollback) 的 write 记录 (场景二)
1.2搜索到 Commit 记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回 OK
1.3 搜索到 Rollback 记录的话,说明本事务已经回滚,会返回 WriteConflict
1.4 None 记录,也就是没有找到本事务的记录,会返回 KeyIsLocked 错误,附带 lock 信息,等待后续 CheckTxnStatus 查看 lock 对应的事务状态
以上述 Bob and Joe’s 事务 t0 为例,t0 已经 Prewrite ,此时状态结果是:

这个时候,如果因为网络原因,client 没有收到 tikv 返回的 Prewrite Resp,因此 tidb 重试重新发送了 Prewrite 请求:
发现其中一个 Key Bob 已经被加锁,
发现这个 lock 是本事务的 (lock.ts=t.start_ts)
接口重复调用,保持幂等,返回 OK
以上述 Bob and Joe’s 事务 t0 为例,t0 已经 Commit the secondary,其 start_ts=7,commit_ts=8 结果是:

又有 t1 事务,目标是扣除 Joe 的账户 7 元,事务 t1 的 start_ts 是 9,commit_ts=10
又有 t2 事务,目标是给 Joe 的账户转账 6 元,事务 t2 的 start_ts 是 10,TIKV 刚刚处理完 Prewrite 请求,此时事务的存储状态为:

这个时候,如果因为网络原因,tikv 又收到了对 t0 (start_ts=7) 的 Prewrite 请求:
检查 Key Joe 存在锁,而且这个 lock 不是本事务的锁 ( lock.ts(9) != start_ts(7) )
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
符合 Commit 记录的条件:record.start_ts=t.start_ts=7
不符合条件,跳过
搜索到一个记录 record.commit_ts=10,record.start_ts=9
搜索到一个记录 record.commit_ts=8,record.start_ts=7
接口重复调用,保持幂等,返回 OK
以上述 Bob and Joe’s 事务 t0 为例,事务 t0 的 start_ts 是 7,t0 由于某些原因已经 rollback ,其结果是:

又有 t1 事务,目标是扣除 Joe 的账户转账 6 元,事务 t1 的 start_ts 是 9,commit_ts=10
又有 t2 事务,目标是给扣除 Joe 的账户转账 2 元,事务 t2 的 start_ts 是 11,TIKV 刚刚处理完 Prewrite 请求,此时事务的存储状态为:

这个时候,如果因为网络原因,tikv 又收到了对 t0 (start_ts=7) 的 Prewrite 请求:
检查 Key Joe 存在锁,而且这个 lock 不是本事务的锁 ( lock.ts(11) != t.start_ts(7) )
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
符合 Rollback 记录的条件:record.start_ts = t.start_ts && record.type = Rollback
不符合条件,跳过
搜索到一个记录 record.commit_ts=10,record.start_ts=9
搜索到一个记录 record.commit_ts=7,record.start_ts=7, record.type=rollback
返回 WriteConflict
以上述 Bob and Joe’s 事务 t0 为例,t0 由于某些原因已经 rollback ,其 start_ts=8,其结果是:

又有 t1 事务,目标是为 Joe 的账户转账 6 元,事务 t1 的 start_ts 是 7,commit_ts是 8,已经提交完毕
值得注意的是,此时 t0.start_ts = t1.commit_ts

我们发现 t1 事务的 Joe 的 commit write 记录和 t0 事务的 rollback 记录重叠了,因此 TIKV 会对 t1 的 commit 记录添加一个标志: has_overlapped_rollback=true
又有 t2 事务,目标是给扣除 Joe 的账户 2 元,事务 t2 的 start_ts 是 9,commit_ts 是 10
又有 t3 事务,目标是给扣除 Joe 的账户 2 元,TIKV 刚刚处理完 t3 的 Prewrite 请求,事务 t3 的 start_ts 是 11, 此时事务的存储状态为:

这个时候,如果因为网络原因,tikv 又收到了对 t0 (start_ts=8) 的 Prewrite 请求:
检查 Key Joe 存在锁,而且这个 lock 不是本事务的锁 ( lock.ts(11) != t.start_ts(8) )
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 8 的记录
符合 Rollback 记录的条件: record.commit_ts = t.start_ts && has_overlapped_rollback = true
不符合条件,跳过
搜索到一个记录 record.commit_ts=10,record.start_ts=9
搜索到一个记录 record.commit_ts=8,record.start_ts=7, has_overlapped_rollback = true
返回 WriteConflict
以上述 Bob and Joe’s 事务 t0 为例,start_ts 为 8, t0 刚刚进行 Prewrite 成功, 状态结果是:

假如此时有个和 t0 并行的事务 t1,start_ts 为 7, 目标是扣除 Joe 的账户 4 元,。
此时对 t1 进行 Prewrite 后,扫描到 Joe t0 事务的 secondary lock 记录
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
搜索到一个记录 record.commit_ts=6,record.start_ts=5
不符合条件,结束搜索,write_ts 并没有 Joe ts 为 8 的记录
返回 KeyIsLocked 错误,等待后续调用 CheckTxnStatus 检查 t0 事务状态
以上述 Bob and Joe’s 事务 t0 为例,start_ts 为 8,commit_ts 为 9, t0 已经 Commit the primary 成功, 状态结果是:

假如此时有个和 t0 并行的事务 t1,start_ts 为 7, 目标是扣除 Joe 的账户 4 元。
此时对 t1 进行 Prewrite 后,扫描到 Joe t0 事务的 secondary lock 记录
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
搜索到一个记录 record.commit_ts=6,record.start_ts=5
不符合条件,结束搜索,write_ts 并没有 Joe ts 为 8 的记录
返回 KeyIsLocked 错误,等待后续调用 CheckTxnStatus 检查 t0 事务状态
以上述 Bob and Joe’s 事务 t0 为例,start_ts 为 8,commit_ts 为 9, 已经提交完毕。
又有 t2 事务,目标是扣除 Joe 的账户 2 元,事务 t2 的 start_ts 是 10,commit_ts 是 11,已经提交完毕
又有 t3 事务,目标是扣除 Joe 的账户 2 元,TIKV 刚刚处理完 t3 的 Prewrite 请求,事务 t3 的 start_ts 是 12, 此时事务的存储状态为:

假如此时有个并行的事务 t1,start_ts 为 7, 目标是扣除 Joe 的账户 4 元。
检查 Key Joe 存在锁,而且这个 lock 不是本事务的锁 ( lock.ts(12) != t.start_ts(7) )
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
已经不符合 commit_ts >= 7
搜索结束
不符合条件,跳过
不符合条件,跳过
搜索到一个记录record.commit_ts=11,record.start_ts=10
搜索到一个记录 record.commit_ts=9,record.start_ts=8
搜索到一个记录 record.commit_ts=6,record.start_ts=5
返回 KeyIsLocked
以上述 Bob and Joe’s 事务 t0 为例,事务 t0 的 start_ts 是 7,commit_ts 是 8,已经提交
又有 t2 事务,目标是扣除 Joe 的账户 2 元,事务 t2 的 start_ts 是 9,commit_ts 是 10,已经提交完毕
又有 t3 事务,目标是扣除 Joe 的账户 2 元,TIKV 刚刚处理完 t3 的 Prewrite 请求,事务 t3 的 start_ts 是 11, 此时事务的存储状态为:

这个时候,出现了 t1 事务,目标是 Joe 的账户转账 6 元,事务 t1 的 start_ts 是 8
值得注意的是,此时 t0.commit_ts = t1.start_ts= 8
tikv 又收到了对 t1 (start_ts=8) 的 Prewrite 请求:
检查 Key Joe 存在锁,而且这个 lock 不是本事务的锁 ( lock.ts(11) != t.start_ts(8) )
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 8 的记录
符合 None 记录的条件: record.commit_ts = t.start_ts && has_overlapped_rollback = false
不符合条件,跳过
搜索到一个记录 record.commit_ts=10,record.start_ts=9
搜索到一个记录 record.commit_ts=8,record.start_ts=7, has_overlapped_rollback = false
返回 KeyIsLocked
Prewrite 的 WRITE 前置检查失败的情况下,并不是直接报错,而是会进行进一步的检查。
如果发现其中一个 Key 的 write_cf 已经有新的记录 (record.commit_ts >= t.start_ts)
继续搜索 write_cf 中是否含有本事务的记录
None 记录指的是:
符合条件 (record.commit_ts = t.start_ts && has_overlapped_rollback = false ) 的 write 记录
或者,符合条件 ( record.start_ts != t.start_ts && record.commit_ts != t.start_ts) 的 write 记录 (场景四)
Rollback 记录指的是:
符合条件 ( record.start_ts = t.start_ts && record.type = Rollback) 的 write 记录 (场景三)
或者,符合条件 (record.commit_ts = t.start_ts && has_overlapped_rollback = true ) 的 write 记录
Commit 记录是指:
( record.start_ts = t.start_ts && record.type != Rollback) 的 write 记录 (场景一、场景二)
(2.1)如果是 Commit 记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回 OK
(2.2)如果是 Rollback 记录的话,说明本事务已经回滚,会返回 WriteConflict
(2.3)没有找到本事务的记录,说明有其他事务并行更新,会返回 WriteConflict,可能需要业务重试事务
由于 Write 的异常场景检查和 Lock 的异常场景检查类似,下面只列举了几个比较典型的 Write 的异常检查场景,其他场景可以参考 Lock 的异常。
以上述 Bob and Joe’s 事务 t0 为例,t0 已经 Commit the secondary,其 start_ts=7,commit_ts=8 结果是:

这个时候,如果因为网络原因,tikv 又收到了对 t0 (start_ts=7) 的 Prewrite 请求:
检查 Key Joe 没有锁
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
符合 Commit 记录的条件:record.start_ts=t.start_ts=7
搜索到一个记录 record.commit_ts=8,record.start_ts=7
接口重复调用,保持幂等,返回 OK
以上述 Bob and Joe’s 事务 t0 为例,t0 已经 Commit the secondary,其 start_ts=7,commit_ts=8
又有 t1 事务,目标是扣除 Joe 的账户 7 元,事务 t1 的 start_ts 是 9,commit_ts=10,已经提交完毕。
此时事务的存储状态为:

这个时候,如果因为网络原因,tikv 又收到了对 t0 (start_ts=7) 的 Prewrite 请求:
检查 Key Joe 没有锁
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
符合 Commit 记录的条件:record.start_ts=t.start_ts=7
不符合搜索条件,跳过
搜索到一个记录 record.commit_ts=10,record.start_ts=9
搜索到一个记录 record.commit_ts=8,record.start_ts=7
接口重复调用,保持幂等,返回 OK
以上述 Bob and Joe’s 事务 t0 为例,事务 t0 的 start_ts 是 7,t0 由于某些原因已经 rollback ,其结果是:

又有 t1 事务,目标是扣除 Joe 的账户转账 6 元,事务 t1 的 start_ts 是 9,commit_ts=10

检查 Key Joe 没有锁
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 7 的记录
符合 Rollback 记录的条件:record.start_ts = t.start_ts && record.type = Rollback
不符合搜索条件,跳过
搜索到一个记录 record.commit_ts=10,record.start_ts=9
搜索到一个记录 record.commit_ts=7,record.start_ts=7, record.type=rollback
返回 WriteConflict
以上述 Bob and Joe’s 事务 t0 为例,t0 的 start_ts=7,commit_ts=9,t0 已经 Commit the secondary成功, 状态结果是:

假如此时有个和 t0 并行的事务 t1,事务 t1 的 start_ts 是 8,目标是扣除 Joe 的账户 4 元
此时对 t1 进行 Prewrite 后,没有扫描到 Joe t0 事务的 lock 记录
继续搜索 write_cf 数据
检查到 Joe 有 commit_ts >= 8 的记录
commit_ts<7
搜索结束
不符合搜索条件,跳过
扫描到了Joe record.commit_ts=9,record.start_ts=7
扫描到了Joe record.commit_ts=6,record.start_ts=5
返回 WriteConflict
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系小编 edito_r@163.com 处理,核实后本网站将在24小时内删除侵权内容。