黄东旭解析 TiDB 的核心优势
769
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_ts
of 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
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。