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_ts
of 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
请求:
检查
Key
Joe
存在锁,而且这个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
请求:
检查
Key
Joe
存在锁,而且这个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
请求:
检查
Key
Joe
存在锁,而且这个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
后,扫描到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
事务状态
场景六: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
后,扫描到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
事务状态
场景七: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 元。
检查
Key
Joe
存在锁,而且这个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
请求:
检查
Key
Joe
存在锁,而且这个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
请求:
检查
Key
Joe
没有锁继续搜索
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
请求:
检查
Key
Joe
没有锁继续搜索
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
检查
Key
Joe
没有锁继续搜索
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
后,没有扫描到Joe
t0
事务的lock
记录继续搜索
write_cf
数据检查到
Joe
有commit_ts >= 8
的记录扫描到了
Joe
record.commit_ts=9
,record.start_ts=7
不符合搜索条件,跳过
扫描到了
Joe
record.commit_ts=6
,record.start_ts=5
commit_ts<7
搜索结束
返回 WriteConflict
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/df11d2ca84c42803cfdf9f257】。文章转载请联系作者。
评论