前言
上一篇文章大致讲了 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
记录的话,说明本事务已经回滚,会返回 WriteConflict-
Rollback
记录指的是:- 符合条件 (
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
记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回 OK-
Commit
记录是指:- (
record.start_ts = t.start_ts
&&record.type
!=Rollback
) 的write
记录 (场景一
、场景二
)
- (
-
-
(
2.2
)如果是Rollback
记录的话,说明本事务已经回滚,会返回 WriteConflict-
Rollback
记录指的是:- 符合条件 (
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