TIKV 分布式事务 --Prewrite 接口详解
作者: TiDBer_qYky5Dvm 原文来源:https://tidb.net/blog/c652507b
前言
上一篇文章大致讲了 TIKV 的分布式事务基本原理,还有几个分布式事务的接口大概逻辑:
https://tidb.net/blog/e5e5ae0d
上个文章中,对于 Prewrite 接口遇到的异常情况,只举了两个非常典型的场景。
本篇文章着重更详细的介绍 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 前,需要对 Prewrite 涉及的 KEY 都要进行如下检查:
检查在
lock_cf中没有记录,也就是没有锁检查在
write_cf中没有大于等于当前事务start_ts的记录
前置检查通过样例
例如下面的例子 (start_ts=7) 就可以通过前置检查:
可以看到存在两个 KEY,一个是 Bob,一个是 Joe。两个 KEY 的 lock_cf 都是空的,同时 write_cf 的最新记录是 6,小于 start_ts(7)
LOCK 检查失败样例
例如下面的例子 (start_ts=7) ,前置检查就会失败,因为 Bob 的 lock_cf 存在一个 ts 为 9 的 primary lock:
WRITE 检查失败样例
例如下面的例子 (start_ts=7) ,前置检查就会失败,因为 Bob 的 write_cf 存在一个 commit_ts=9 的记录:
Prewrite —— 操作
主要流程
前置检查通过后,我们开始进行真正的 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 —— 异常检查
上面所述都是比较乐观的场景,但是现实上可能会遇到各种并发问题或者网络问题,导致 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 异常检查
Prewrite 的 LOCK 前置检查失败的情况下,例如下图中 Bob 这个 KEY 就存在着一个 primary lock:
并不是直接报错,而是会进行进一步的检查。
主要流程
如果发现其中一个 Key 已经被加锁,判断这个 lock 是不是本事务的 (lock.ts=t.start_ts)
1.1如果是的话,那么就是接口重复调用,保持幂等,返回 OK (场景一)否则的话,说明这个
lock不是本事务的,需要根据t.start_ts继续搜索write_cf中的write记录1.2搜索到Commit记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回 ***OK ***Commit记录是指:(
record.start_ts = t.start_ts&&record.type!=Rollback) 的write记录 (场景二)1.3搜索到Rollback记录的话,说明本事务已经回滚,会返回 WriteConflictRollback记录指的是:符合条件 (
record.start_ts = t.start_ts&&record.type=Rollback) 的write记录 (场景三)或者,符合条件 (
record.commit_ts = t.start_ts&&has_overlapped_rollback = true )的write记录 (场景四)1.4 None记录,也就是没有找到本事务的记录,会返回 KeyIsLocked 错误,附带lock信息,等待后续CheckTxnStatus查看lock对应的事务状态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记录 (场景八)
场景样例
场景一:Prewritten
以上述 Bob and Joe’s 事务 t0 为例,t0 已经 Prewrite ,此时状态结果是:
这个时候,如果因为网络原因,client 没有收到 tikv 返回的 Prewrite Resp,因此 tidb 重试重新发送了 Prewrite 请求:
发现其中一个
Key Bob已经被加锁,发现这个
lock是本事务的 (lock.ts=t.start_ts)接口重复调用,保持幂等,返回 OK
场景二:Committed
以上述 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 请求:
检查
KeyJoe存在锁,而且这个lock不是本事务的锁 (lock.ts(9) != start_ts(7))继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=10,record.start_ts=9不符合条件,跳过
搜索到一个记录
record.commit_ts=8,record.start_ts=7符合
Commit记录的条件:record.start_ts=t.start_ts=7接口重复调用,保持幂等,返回 OK
场景三:Rollbacked
以上述 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 请求:
检查
KeyJoe存在锁,而且这个lock不是本事务的锁 (lock.ts(11) != t.start_ts(7))继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=10,record.start_ts=9不符合条件,跳过
搜索到一个记录
record.commit_ts=7,record.start_ts=7,record.type=rollback符合
Rollback记录的条件:record.start_ts = t.start_ts&&record.type=Rollback返回 WriteConflict
场景四:Rollbacked
以上述 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 请求:
检查
KeyJoe存在锁,而且这个lock不是本事务的锁 (lock.ts(11) != t.start_ts(8))继续搜索
write_cf数据检查到
Joe有commit_ts >= 8的记录搜索到一个记录
record.commit_ts=10,record.start_ts=9不符合条件,跳过
搜索到一个记录
record.commit_ts=8,record.start_ts=7,has_overlapped_rollback = true符合
Rollback记录的条件:record.commit_ts = t.start_ts&&has_overlapped_rollback = true返回 WriteConflict
场景五:None
以上述 Bob and Joe’s 事务 t0 为例,start_ts 为 8, t0 刚刚进行 Prewrite 成功, 状态结果是:
假如此时有个和 t0 并行的事务 t1,start_ts 为 7, 目标是扣除 Joe 的账户 4 元,。
此时对 t1 进行
Prewrite后,扫描到Joet0事务的secondary lock记录继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=6,record.start_ts=5不符合条件,结束搜索,
write_ts并没有Joets为 8 的记录返回
KeyIsLocked错误,等待后续调用CheckTxnStatus检查t0事务状态
场景六:None
以上述 Bob and Joe’s 事务 t0 为例,start_ts 为 8,commit_ts 为 9, t0 已经 Commit the primary 成功, 状态结果是:
假如此时有个和 t0 并行的事务 t1,start_ts 为 7, 目标是扣除 Joe 的账户 4 元。
此时对 t1 进行
Prewrite后,扫描到Joet0事务的secondary lock记录继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=6,record.start_ts=5不符合条件,结束搜索,
write_ts并没有Joets为 8 的记录
返回
KeyIsLocked错误,等待后续调用CheckTxnStatus检查t0事务状态
场景七:None
以上述 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 元。
检查
KeyJoe存在锁,而且这个lock不是本事务的锁 (lock.ts(12) != t.start_ts(7))继续搜索
write_cf数据检查到
Joe有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已经不符合
commit_ts >= 7搜索结束
返回 KeyIsLocked
场景八:None
以上述 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 请求:
检查
KeyJoe存在锁,而且这个lock不是本事务的锁 (lock.ts(11) != t.start_ts(8))继续搜索
write_cf数据检查到
Joe有commit_ts >= 8的记录搜索到一个记录
record.commit_ts=10,record.start_ts=9不符合条件,跳过
搜索到一个记录
record.commit_ts=8,record.start_ts=7,has_overlapped_rollback = false符合
None记录的条件:record.commit_ts = t.start_ts&&has_overlapped_rollback = false返回 KeyIsLocked
Prewrite —— WRITE 异常检查
Prewrite 的 WRITE 前置检查失败的情况下,并不是直接报错,而是会进行进一步的检查。
主要流程
如果发现其中一个 Key 的 write_cf 已经有新的记录 (record.commit_ts >= t.start_ts)
继续搜索
write_cf中是否含有本事务的记录(
2.1)如果是Commit记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回 OKCommit记录是指:(
record.start_ts = t.start_ts&&record.type!=Rollback) 的write记录 (场景一、场景二)(
2.2)如果是Rollback记录的话,说明本事务已经回滚,会返回 WriteConflictRollback记录指的是:符合条件 (
record.start_ts = t.start_ts&&record.type=Rollback) 的write记录 (场景三)或者,符合条件 (
record.commit_ts = t.start_ts&&has_overlapped_rollback = true )的write记录(
2.3)没有找到本事务的记录,说明有其他事务并行更新,会返回 WriteConflict,可能需要业务重试事务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记录 (场景四)
场景样例
由于 Write 的异常场景检查和 Lock 的异常场景检查类似,下面只列举了几个比较典型的 Write 的异常检查场景,其他场景可以参考 Lock 的异常。
场景一:Committed
以上述 Bob and Joe’s 事务 t0 为例,t0 已经 Commit the secondary,其 start_ts=7,commit_ts=8 结果是:
这个时候,如果因为网络原因,tikv 又收到了对 t0 (start_ts=7) 的 Prewrite 请求:
检查
KeyJoe没有锁继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=8,record.start_ts=7符合
Commit记录的条件:record.start_ts=t.start_ts=7接口重复调用,保持幂等,返回 OK
场景二:Committed
以上述 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 请求:
检查
KeyJoe没有锁继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=10,record.start_ts=9不符合搜索条件,跳过
搜索到一个记录
record.commit_ts=8,record.start_ts=7符合
Commit记录的条件:record.start_ts=t.start_ts=7接口重复调用,保持幂等,返回 OK
场景三:Rollbacked
以上述 Bob and Joe’s 事务 t0 为例,事务 t0 的 start_ts 是 7,t0 由于某些原因已经 rollback ,其结果是:
又有 t1 事务,目标是扣除 Joe 的账户转账 6 元,事务 t1 的 start_ts 是 9,commit_ts=10
检查
KeyJoe没有锁继续搜索
write_cf数据检查到
Joe有commit_ts >= 7的记录搜索到一个记录
record.commit_ts=10,record.start_ts=9不符合搜索条件,跳过
搜索到一个记录
record.commit_ts=7,record.start_ts=7,record.type=rollback符合
Rollback记录的条件:record.start_ts = t.start_ts&&record.type=Rollback返回 WriteConflict
场景四:None
以上述 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后,没有扫描到Joet0事务的lock记录继续搜索
write_cf数据检查到
Joe有commit_ts >= 8的记录扫描到了
Joerecord.commit_ts=9,record.start_ts=7不符合搜索条件,跳过
扫描到了
Joerecord.commit_ts=6,record.start_ts=5commit_ts<7搜索结束
返回 WriteConflict
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/df11d2ca84c42803cfdf9f257】。文章转载请联系作者。







评论