前言
关于分布式事务基本原理,相关资料比较多写的比较全面,这里罗列一些需要了解的基本原理文章:
首先我们需要了解传统的分布式事务模型,2PC
与 3PC
:
https://developer.aliyun.com/article/1199985
接下来我们需要了解一下,Google
的 Percolator
事务模型:
https://tikv.org/deep-dive/distributed-transaction/percolator/
https://zhuanlan.zhihu.com/p/261115166
我们看一下 TIKV
实现的 Percolator
事务模型:
https://cn.pingcap.com/blog/tidb-transaction-model/
https://cn.pingcap.com/blog/tikv-source-code-reading-12/
最后,TIKV
Percolator
事务模型下数据的读取:
https://cn.pingcap.com/blog/tikv-source-code-reading-13/
2PC 与 3PC
对于传统的事务模型来说,一般都有两种角色,coordinator
(协调者) 和 participant
(参与者)
2PC
有个无法避免的 case
:
-
假如事务参与者
participant
有 3 个,分别是p1
,p2
,p3
,协调者有一个c1
-
事务过程中,
p2
、p3
已经Prepare
成功,事务状态有以下几种可能:p1
还未收到Prepare
,当前事务总状态为Prepare
- 或者,
p1
Prepare
成功,还未收到协调者Commit/Rollback
请求,当前事务总状态为Prepare
- 或者,
p1
Prepare
失败,协调者c1
向p1
发送了Rollback
请求,p1
返回ok
,当前事务总状态为Rollback
- 或者,
p1
Prepare
成功, 协调者c1
向p1
发送Commit
请求,p1
返回ok
,当前事务总状态为Commit
-
协调者
c1
与事务参与者p1
全部Down
-
协调者
c2
被启动,这个时候,c2
查询p2
、p3
的状态为Prepare
,但是事务总状态完全无法肯定,Prepare/Commit/Rollback
均有可能,只能等待p1
服务或者c1
的故障恢复后才能完全确定事务状态。
3PC
通过在 Prepare
和 Commit
中间添加一个 PreCommit
状态来解决这个问题。
当 c1
与 p1
都 down
的状态下,新启动的 c2
查询 p2
、p3
的当前状态就可以确定当前事务状态
-
假如
p2
、p3
都是Prepare
状态的话,-
p1
的状态可能是Prepare
或者PreCommit
,不可能是Commit
或者Rollback
- 所以事务都不算生效,可以放心回滚事务
-
-
假如
p2
、p3
分别是PreCommit
、Prepare
状态的话-
p1
的状态可能是Prepare
或者PreCommit
,不可能是Commit
或者Rollback
- 所以事务都不算生效,可以放心的回滚事务
-
-
假如
p2
、p3
都是PreCommit
状态的话,说明p1
、p2
、p3
都Prepare
成功,-
p1
的状态可能是Prepare、PreCommit
或者Commit
,不可能是Rollback
- 由于
p1
可能已经提交,因此需要提交事务
-
然而 3PC
状态下,多了一次交互,性能肯定会有所下降,而且也无法解决网络分区的问题:
- 假如事务参与者
participant
有 3 个,分别是p1
,p2
,p3
,协调者有一个c1
p1
,p2
已经Precommit
成功,p3 还未Precommit
, 这时候发生网络分区状况,p3
被单独隔离到一个网络分区p1
,p2
选举出coordinator
c2
,c2
查询p1
、p2
状态是Precommit
后,提交了事务p3
选举出c3
,c3
查询p3
状态为Prepare
状态,回滚了事务- 事务的状态存在不一致的问题
Percolator 事务模型
对于 Percolator
事务模型来说,已经不存在传统意义的 coordinator
(协调者) 和 participant
(参与者),所有的事务状态都存储在参与者中。
也可以说 coordinator
不再存储 Prewrite
、Commit
、Rollback
状态,所有的状态都存储在参与者 participant
中。
Percolator
实现分布式事务主要基于3个实体:Client
、TSO
、BigTable
。
Client
是事务的发起者和协调者TSO
为分布式服务器提供一个精确的,严格单调递增的时间戳服务。- BigTable 是
Google
实现的一个分布式存储的
Percolator
事务模型是 2PC
的一种实现方式,为了解决 2PC
的容灾问题,参与者 participant
会将 Prepare
、Commit
等状态通过分布式协议 RAFT
、Paxos
进行分布式存储。确保参与者 participant
即使 Fail Down
,恢复回来以后事务状态不会丢失。
还是以之前的例子:
-
假如事务参与者
participant
有 3 个,分别是p1
,p2
,p3
,协调者有一个c1
-
事务过程中,
p2
、p3
已经Prewrite
成功p1
还未收到Prewrite
,当前事务总状态为Prewrite
- 或者,
p1
Prewrite
成功,还未收到协调者Commit/Rollback
请求,当前事务总状态为Prewrite
- 或者,
p1 Prewrite
成功,协调者c1
向p1
发送Commit
请求,p1 通过 RAFT 协议同步事务状态后, 当前事务总状态为Commit
- 或者,
p1 Prewrite
失败,协调者c1
向p1
发送了Rollback
请求,p1
通过RAFT
协议同步事务状态后,当前事务总状态为Rollback
-
协调者
c1
与事务参与者p1
全部Down
-
协调者
c2
被启动,参与者p1
虽然Down
,但是会有容灾节点p1-1
被启动。c2
查询p1-1
节点的存储状态- 如果
p1-1
的状态为None
,那么可以放心的Rollback
- 如果
p1-1
的状态为Prewrite
,那么可以放心的Rollback
- 如果
p1-1
的状态为Rollback
,那么可以放心的Rollback
- 如果
p1-1
的状态为Commit
, 那么必须进行Commit
- 如果
在
2PC
中,最关键的莫过于Commit Point
(提交点)。因为在
Commit Point
之前,事务都不算生效,并且随时可以回滚。而一旦过了Commit Point
,事务必须生效,哪怕是发生了网络分区、机器故障,一旦恢复都必须继续下去。
事务的流程
由于采用的是乐观事务模型,写入会缓存到一个 buffer 中,直到最终提交时数据才会被写入到 TiKV;
而一个事务又应当能够读取到自己进行的写操作,因而一个事务中的读操作需要首先尝试读自己的 buffer,如果没有的话才会读取 TiKV。
当我们开始一个事务、进行一系列读写操作、并最终提交时,在 TiKV 及其客户端中对应发生的事情如下表所示:
Percolator
事务模型举例: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. The first step is
Prewrite
:
- Get the
start_ts
of the transaction. In our example, it’s7
.- For each row involved in this transaction, put a lock in the
lock
column, and write the data to thedata
column. One of the locks will be chosen as the primary lock.After
Prewrite
, our data looks like this:Then
Commit
:
- Get the
commit_ts
, in our case,8
.- Commit the primary: Remove the primary lock and write the commit record to the
write
column.
- Commit all secondary locks to complete the writing process.
TIKV 事务接口概览
这里大致写一下乐观事务中,2PC
的大致流程,各个接口的详细逻辑与样例场景可以参考后续文章。
Prewrite 接口
2PC
的第一阶段,预提交。目的是将事务涉及的多个 KEY-VALUE
写入 default_cf
,同时将在 lock_cf
上加锁
主要流程
- 检查在
lock_cf
中没有记录,也就是没有锁 - 检查在
write_cf
中没有大于等于当前事务start_ts
的记录 - 将
KEY-VALUE
写入default_cf
- 将
lock
信息写入lock_cf
上加锁
样例讲解
以上述 Bob and Joe’s
事务 t0
为例,t0
开始之前,Bob
有 10 元,Joe
有 2 元
Bob and Joe’s
事务 t0
目标是 Bob
转账给 Joe
7 元,Bob
就变成了 3 元,Joe
变成了 9 元。
Prewrite
后的结果是:
值得注意的是,tidb
指定 Bob
是 primary key
,Bob
写入的 lock
是 primary lock
。指定 Joe
是 secondary key
,Joe
写入的 lock
是 secondary lock
。
通过 Joe
的 secondary lock
我们可以定位到其 primary
key
是 Bob
。Bob
的当前状态代表了整个事务 t0
当前的状态
异常场景
-
如果发现其中一个
Key
已经被加锁,判断这个lock
是不是本事务的 (lock.ts=t.start_ts
)-
如果是的话,那么就是接口重复调用,保持幂等,返回
OK
-
否则的话,说明这个 lock 不是本事务的,需要继续搜索
write_cf
中是否含有本事务的记录 (record.start_ts = t.start_ts | record.commit_ts = t.start_ts
)- 搜索到
Commit
记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回OK
- 搜索到
Rollback
记录的话,说明本事务已经回滚,会返回WriteConflict
- 没有找到本事务的记录,会返回
KeyIsLocked
错误,附带lock
信息,等待后续CheckTxnStatus
查看lock
对应的事务状态 (异常场景一
)
- 搜索到
-
-
如果发现其中一个
Key
的write_cf
已经有新的记录 (record.commit_ts >= t.start_ts
)-
继续搜索
write_cf
中是否含有本事务的记录 (record.start_ts = t.start_ts | record.commit_ts = t.start_ts
)- 如果是
Commit
记录的话,说明本事务已经提交,那么就是接口重复调用,保持幂等,返回 OK - 如果是
Rollback
记录的话,说明本事务已经回滚,会返回WriteConflict
- 没有找到本事务的记录,说明有其他事务并行更新,会返回
WriteConflict
,可能需要业务重试事务 (异常场景二
)
- 如果是
-
异常场景样例
由于 Prewrite
的异常场景过多,我们这里只举两个非常典型的场景,其他场景可以查看后续 Prewrite
详解文章。
场景一:KeyIsLocked
以上述 Bob and Joe’s
事务 t0 为例,t0 已经 Commit the primary
成功, 状态结果是:
假如此时有个和 t0
并行的事务 t1,目标是扣除 Joe
的账户 4 元,start_ts
为 8。
- 此时对 t1 进行
Prewrite
后,扫描到Joe
t0
事务的secondary lock
记录 - 同时
write_ts
并没有Joe
ts
为 8 的记录 - 返回
KeyIsLocked
错误,等待后续调用CheckTxnStatus
检查t0
事务状态
场景二:WriteConflict
以上述 Bob and Joe’s
事务 t0 为例,t0 已经 Commit the secondary
成功, 状态结果是:
假如此时有个和 t0
并行的事务 t1
,目标是扣除 Joe
的账户 4 元,事务 t1 的 start_ts
是 8
- 此时对 t1 进行
Prewrite
后,没有扫描到Joe
t0
事务的lock
记录 - 扫描到了
Joe
commit_ts
是 9 的commit_ts
记录 - 继续搜索
-
write_ts
没有扫描到Joe
ts
是 8 的记录 - 因此返回了
WriteConflict
错误。
CheckTxnStatus 接口
如果 Prewrite
失败,返回 KeyIsLocked
,那么 tidb
可能会调用 CheckTxnStatus
接口来查看 lock
涉及的 primary key
当前状态
主要流程
-
如果
primary
key
的lock
已经被清理,同时write_cf
存在提交记录 (场景一
)- 说明
lock
涉及的primary
key
已经提交,代表整个事务已经提交 - 返回
committed_ts
等待tidb
调用ResolveLock
接口将lock
涉及的secondary
key
也进行提交
- 说明
-
如果
primary
key
的lock
已经被清除,同时 write_cf 存在回滚记录- 说明
lock
的primary
key
已经回滚,代表整个事务已经回滚 - 返回 0 (代表事务已回滚),等待
tidb
调用ResolveLock
接口将lock
的secondary
key
也进行回滚
- 说明
样例讲解
场景一:committed
以上述 Bob and Joe’s
事务 t0
为例,t0
Commit the primary
后的结果是:
此时 t0
已经完成了 primary key (Bob)
的 Commit
,还未来得及对 secondary key (Joe)
进行 commit
。
假如此时有个和 t0
并行的事务 t1
,目标是扣除 Joe
的账户 4 元。
-
此时对
t1
进行Prewrite
后,扫描到Joe
的secondary
lock
记录,返回了KeyIsLocked
错误。 -
tidb
将会通过Joe
的lock
查询到t0
事务的primary
key
,也就是Bob
- 调用
CheckTxnStatus
来查看Bob
此时的状态。 -
CheckTxnStatus
发现了Bob
的write_cf
的Commit
记录 - 确认事务
t0
已经提交,向tidb
返回了t0
的committed_ts(8)
- 调用
-
tidb
将会利用committed_ts(8)
调用ResolveLocks
,对Joe
这个secondary
key
进行t0
事务commit
secondary
操作。 -
最后
tidb
对Bob
这个key
进行t1
Prewrite
重试
tidb
利用committed_ts
调用ResolveLocks
后,Joe
这个t0
的secondary
key
会被提交
异常场景
如果 primary
key
的 lock
还存在,那么查看 primary key lock
的状态
-
如果
primary
key
的lock
已经过期 (场景一
)- 说明
primary
key
相关事务已经Down
了,需要对该事务进行回滚 - 对
primary
key
进行回滚 - 返回 0 (代表事务已回滚),等待
tidb
调用ResolveLock
接口将lock
的secondary
key
也进行回滚
- 说明
-
如果
primary
key
的lock
还未过期(场景二
)- 说明本事务和其他事务存在并发,需要等待
- 返回
uncommitted
,tidb
将会等待一段时间后重新调用CheckTxnStatus
接口
异常场景样例
场景一:lock 已过期
以上述 Bob and Joe’s
事务 t0
为例,假如目前 t0
Priwrite
已完成,但是 t0
被异常阻塞,目前状态结果是:
由于 t0
事务的异常阻塞,其中 Bob
、Joe
的 lock
TTL
已经超时。
假如此时有个和 t0
并行的事务 t1
,目标是扣除 Joe
的账户 4 元。
-
此时对
t1
进行Prewrite
后,扫描到Joe
t0
事务的secondary
lock
记录,返回了KeyIsLocked
错误。 -
tidb
将会通过Joe
的lock
查询到t0
事务的primary
key
,也就是Bob
,调用CheckTxnStatus
来查看Bob
此时的状态。-
CheckTxnStatus
发现了Bob
的lock_cf
记录,而且lock
已经过期,说明整个t0
事务已经Down
了 - 对
primary key
也就是Bob
进行回滚,包括清除lock_cf
、default_cf
记录,对write_cf
写入rollback
记录 - 返回结果 0,代表事务
t0
已经回滚完成
-
-
tidb
收到 结果 0 后,调用ResolveLocks
,对Joe
这个secondary
key
也进行t0
事务rollback
secondary
操作。 -
最后
tidb
对Bob
这个key
进行t1
Prewrite
重试
tidb
调用 CheckTxnStatus
前,t0
事务状态:
由于 Bob
的 primary
lock
已经过期,tidb
调用 CheckTxnStatus
后,t0
事务状态:
可以看到 t0
的 primary
key
也就是 Bob
已经被回滚,lock_cf
、default_cf
被清理, write_cf
被追加 rollback
记录
tidb
调用 ResolveLocks
后,t0 的 secondary
key
也就是 Joe
也被回滚,Joe
的 lock_cf
、default_cf
被清理, write_cf
被追加 rollback
记录:
场景二:lock 未过期
以上述 Bob and Joe’s
事务 t0
为例,假如目前 t0
Priwrite
刚刚完成, 目前状态结果是:
假如此时有个和 t0
并行的事务 t1
,目标是扣除 Joe
的账户 4 元。
-
此时对 t1 进行
Prewrite
后,扫描到Joe
t0
事务的secondary
lock
记录,返回了KeyIsLocked
错误。 -
tidb
将会通过Joe
的lock
查询到t0
事务的primary
key
,也就是Bob
,调用CheckTxnStatus
来查看Bob
此时的状态。-
CheckTxnStatus
发现了Bob
的lock_cf
记录,而且lock
还未过期,说明整个t0
事务还未提交 - 返回了
uncommitted
错误
-
-
tidb
收到uncommitted
状态错误后,会等待一段时间后重试CheckTxnStatus
查看t0
状态
ResolveLocks 接口
根据 CheckTxnStatus
接口的返回值,挨个对 lock
绑定的 key
进行提交或者回滚。
主要流程
-
如果
CheckTxnStatus
接口返回了committed_ts
,说明lock
涉及的事务已经提交,ResolveLocks
将会对lock
绑定的secondary
key
进行提交-
如果存在
lock
key
对应的lock_cf
记录,直接执行Commit
提交操作- 去除
lock_cf
记录 - 向
write_cf
写入Commit
记录
- 去除
-
-
如果
CheckTxnStatus
接口返回了 0,说明lock
涉及的事务已经回滚,ResolveLocks
将会对lock
绑定的secondary
key
进行回滚-
如果存在
lock key
对应的lock_cf
记录,直接执行Rollback
回滚操作- 去除
lock_cf
、default_cf
记录 - 向
write_cf
写入Rollback
记录
- 去除
-
样例讲解
场景一:提交事务
tidb
调用 ResolveLocks
前,t0(start_ts=7
) 当前状态是:
可以看到 t0
的 primary key
Bob
已经被提交,Joe
这个 t0
的 secondary
key
还未提交。
tidb
利用 start_ts(7)-committed_ts(8)
调用 ResolveLocks
后,Joe
这个 t0
的 secondary
key
也会被提交:
清除了 Joe
的 lock_cf
记录,添加了 write_cf
Commit
记录
场景二:回滚事务
tidb
调用 ResolveLocks
前,可以看到 t0
(start_ts=7
) 事务的 primary key
已经被回滚:
tidb
利用 start_ts(7)-committed_ts(0)
调用 ResolveLocks
后,可以看到 t0
的 secondary
key
也就是 Joe
也被回滚:
Joe
的 lock_cf
、default_cf
被清理, write_cf
被追加 rollback
记录
异常场景
-
如果
CheckTxnStatus
接口返回了committed_ts
,说明lock
涉及的事务已经提交,ResolveLocks
将会对lock
绑定的secondary
key
进行提交-
如果没有找到
lock
key
对应的lock_cf
记录,进一步去write_cf
去查找记录- 如果在
write_cf
找到了对应的Commit
记录,直接返回即可,说明接口被重复调用 - 如果在
write_cf
找到了回滚记录,返回报错TxnLockNotFound
- 如果在
write_cf
没有找到任何记录,返回报错TxnLockNotFound
- 如果在
-
-
如果
CheckTxnStatus
接口返回了 0,说明lock
涉及的事务已经回滚,ResolveLocks
将会对lock
绑定的secondary
key
进行回滚-
如果没有找到
lock
key
对应的lock_cf
记录,进一步去write_cf
去查找记录- 如果在
write_cf
找到了对应的Rollback
记录,直接返回OK
即可,说明接口被重复调用 - 如果在
write_cf
找到了Commit
记录,返回报错Committed
- 如果在
write_cf
没有找到任何记录,写入回滚记录, 返回ok
- 如果在
-
Commit 接口
当对所有的 key
执行 Prewrite
均成功后,TIDB
将会对事务 t
的 primary
key
执行 commit
操作。当 commit
完成后,标志这事务 t
已经被提交。
这个时候已经可以把提交成功的结果返回给 Client
,后续 TIDB
将会异步对 secondary
key
继续执行 commit
操作
主要流程
-
如果存在
lock
key
对应的lock_cf
记录,直接执行Commit
提交操作- 去除
lock_cf
记录 - 向
write_cf
写入Commit
记录
- 去除
样例讲解
tidb
调用 Commit
前,t0(start_ts=7
) 当前状态是:
tidb
调用 Commit
后,Bob
这个 t0
的 primary
key
会被提交:
清除了 Bob
的 lock_cf
记录,添加了 write_cf
Commit
记录
异常场景
-
如果没有找到
lock
key
对应的lock_cf
记录,进一步去write_cf
去查找记录- 如果在
write_cf
找到了对应的Commit
记录,直接返回即可,说明接口被重复调用 - 如果在
write_cf
找到了回滚记录,返回报错TxnLockNotFound
- 如果在
write_cf
没有找到任何记录,返回报错TxnLockNotFound
- 如果在
Rollback 接口
当事务的某些 key
执行 Prewrite
失败后,TIDB
将会对事务 t
的 key
执行 rollback
操作。
当 rollback
完成后,事务相关 key
被 Prewrite
加上的 lock
将会被清除。
主要流程
-
如果存在
lock key
对应的lock_cf
记录,直接执行Rollback
回滚操作- 去除
lock_cf
、default_cf
记录 - 向
write_cf
写入Rollback
记录
- 去除
样例讲解
tidb
调用 Rollback
前:
tidb
调用 Rollback
后,可以看到 t0
的 key
均被回滚:
异常场景
-
如果没有找到
lock
key
对应的lock_cf
记录,进一步去write_cf
去查找记录- 如果在
write_cf
找到了对应的Rollback
记录,直接返回OK
即可,说明接口被重复调用 - 如果在
write_cf
找到了Commit
记录,返回报错Committed
- 如果在
write_cf
没有找到任何记录,写入回滚记录, 返回ok
- 如果在