TIKV 源码学习笔记 -- 分布式事务接口 Commit/Rollback
作者: ylldty 原文来源:https://tidb.net/blog/2123ce5d
前言
上一篇介绍了 Prewrite 接口,这篇我们继续介绍 Commit/Rollback
接口,Cleanup
接口实际上和 Rollback
接口类似。
除此之外,还有 CheckTxnStatus
/ ResolveLock
/ CheckSecondaryLocks
关键接口,由于篇幅有限,只能后面有机会再聊
Commit
参数
KEYS
:Commit
提交的涉及的 KEYS,相关的KEYS
和Prewrite
相同LOCK_TS
:Commit
需要消除的LOCK TS
,一般也是事务的start_ts
COMMIT_TS
: 提交的最终commit_ts
以 UPDATE
语句为例:
其他语句类似,区别不大,这里不在赘述。
代码简读
对每个
KEY
都调用commit
函数进行提交操作使用
load_lock
函数来检查是否含有KEY
对应的LOCK
,我们预期应该存在Prewrite
留下的LOCK
如果没有发现
LOCK
或者不是本事务的LOCK
,:调用
get_txn_commit_record
观察是否已经提交完毕,如果已经提交,那么可以提前返回OK
如果发现了回滚记录,或者没有找到任何记录,那么返回
ERR: TxnLockNotFound
如果发现了本事务的
LOCK
,首先检查一下lock.min_commit_ts
必须大于commit_ts
如果
LOCK
类型是正常的锁,那么删除锁,并且添加新的write
记录即可正常返回如果
LOCK
类型是悲观锁,这个是非预期的,这时候commit
操作实际上就是删除悲观锁即可 (并不需要write CF
上的回滚记录)。可能是因为
pessimistic rollback
请求未能发送到TIKV
,也可能是TIKV
由于某种情况下突然收到了pessimistic lock
请求,个人理解这些特殊场景可能并不是二阶段过程中会发生的,因为Prewrite
成功后不可能将常规的锁转为悲观锁,根据注释大概率应该是resolve lock
过程中可能遇到的场景还有一种比较特殊的情况,那就是存在并发的两个事务,
t1
与t2
,t1
开启的时间很早,也就是t1.start_ts < t2.start_ts
t1
对KEY
调用了Prewrite
进行了加锁t2
开启的时间比较晚,也想对KEY
进行加锁,发现有并发事务的锁冲突,因此采取了回滚。回滚的时候,会留下
write
记录,该write
记录的write.commit_ts = t2.start_ts
那么其实会有一个隐患,如果
t1
提交的时候,t1.commit_ts
恰好和t2.start_ts
相同的话,那么t1
提交write
记录就会覆盖t2
的回滚记录正常来说,如果在提交
t1
事务的时候,先来看一眼write
现有记录的话,可以简单的避免这个问题。但是每次提交都查询write
记录的话,代价稍微有点高。但是我们每次进行
commit
的时候,都避免不了去加载LOCK
信息因此,引入了
lock.rollback_ts
,每当其他事务发生锁冲突因此需要回滚的时候,我们都会更新这个rollback_ts
数组。如果t1
发现自己的commit_ts
命中了lock.rollback_ts
,那么写write
记录的时候需要小心一些,设置overlapped_rollback
为true
,标志这个write
记录其实是叠加了两个事务的commit
和rollback
Rollback
场景
和直观认知可能不太一样,TIKV
的 Rollback
接口一般情况下并不是 sql
的 rollback
语句触发的。
对于乐观事务来说,由于事务过程中,没有加任何锁,因此 sql
rollback
语句实际上并不需要调用 tikv
的接口处理,只需要将 Buff
的 put
数据清空即可。
对于悲观事务来说,事务过程中加了悲观锁,但是 sql
rollback
语句实际触发的是 pessimistic_rollback
这个接口,专门用于清理悲观锁。
TIKV
的 Rollback
接口常见于乐观事务写冲突的时候,乐观事务在进行二阶段提交过程中,prewrite
过程中发现了写冲突,这时候就需要调用 TIKV
的 Rollback
。
t1: begin optimistic;
t1: DELETE FROM MANAGERS_UNIQUE where FIRST_NAME='Brad7';
t2: begin optimistic;
t2: DELETE FROM MANAGERS_UNIQUE where FIRST_NAME='Brad7';
t2: commit;
t1: commit; ERROR 9007 (HY000): Write conflict;
实际上对于写冲突的 Rollback
, 之前 prewrite
也大概率并没有加锁,因此 Rollback
不需要清理锁,也不需要清楚 default CF
的数据,只需要添加一个 Rollback write
记录。
参数
KEYS
:Commit
提交的涉及的KEYS
,相关的KEYS
和Prewrite
相同LOCK_TS
:Commit
需要消除的LOCK TS
,一般也是事务的start_ts
Overlapped Rollback 回滚记录
我们知道回滚记录是个比较特殊的 write
记录,不仅仅是 write.type
是 rollback
类型的,而且还因为其 write
记录的 commitTS
与事务的 startTS
是相同的,TIKV
这样设计应该是为了减少和 PD
的交互,少获取一次 TS
,节省系统消耗。
因此普通的提交记录是这样的 KEY-VALUE
格式:
{KEY_CommitTS
: { write.type=put,write.startTS=startTS
} }
而回滚记录一般是这样的 KEY-VALUE
格式:
{KEY_StartTS
: { write.type=rollback,write.startTS=startTS
} }
那么这样就会有一个问题,那就是很多事务的 commitTS
也不是 PD
获取的,而是通过计算得到的,例如 Async Commit
。那么就可能会遇到这个场景:
T1
事务启动startTS=start_t1
, 采用了Async Commit
的方式,计算出commitTS=commit_t1
,提交记录的KEY
是KEY_commit_t1
T2
事务启动startTS=start_t2
,然后被回滚,因此其回滚记录的KEY
是KEY_start_t2
由于
commit_t1
并不是 PD 获取的,而start_t2
是 PD 获取的 ts,因此就有概率commit_t1==start_t2
,也就是说两个事务的提交记录和回滚记录在write CF
上重叠了这个时候,就需要一个属性值
Overlapped
,当一个提交记录的Overlapped
为true
的时候,就代表这其实是两个记录,一个提交记录一个回滚记录
保护模式和非保护模式回滚记录
当我们事务冲突很严重的时候,就容易有多条的回滚记录,这对于 TIKV
的 mvcc
扫描来说效率太慢了。因此 TIKV
有个优化,在 write CF
上面,对于一个 KEY
,只保留最新的那个回滚记录即可,其他回滚记录可以直接删除。
但是为了正确性考虑,必须防止已经对 KEY
进行了回滚操作,后面突然由于网络原因又出现对 KEY
调用了 prewrite
和 commit
,导致回滚的事件被错误的提交。
因此只能对部分 KEY
进行这种 collapse
删除优化。
具体的就是对于 rowID
、唯一索引来说,采用保护模式的回滚,该回滚记录不会被删除。这样每次事务被错误的 commit
的时候,都可以通过被保护的回滚记录了解到这个事务实际上已经被提交了。
对于普通索引,采用非保护模式,可能被其他事务更新的 rollback
记录删除,也可能遇到需要 Overlapped
的场景,并不设置 Overlapped
为 true
。
最后实际上最后结果就是:普通索引上面即使被回滚了,但是却找不到任何回滚的记录。
代码简读
对每个
KEY
都调用cleanup
函数进行回滚操作。(而且是以非保护模式下来调用)使用
load_lock
函数来检查是否含有KEY
对应的LOCK
,我们预期应该存在事务留下的LOCK
如果发现了本事务的锁,
lock.ts == txn.start_ts
,执行rollback_lock
进行回滚操作rollback_lock
为了保险起见,会再次通过get_txn_commit_record
函数查看write
的最新记录如果发现
write
上有当前事务的提交记录,直接panic
如果发现有
OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明之前已经添加了write
回滚记录,删除了default
上面的value
数据,那么现在只需要把LOCK
记录删除即可如果没有发现任何提交记录或者回滚记录,那么
如果
LOCK
是PUT
类型、且已经写入default CF Value
,那么需要删除default CF Value
非保护模式下利用
make_rollback
生成rollback
类型的write
记录特别需要注意的是,由于是非保护模式下,所以如果恰好
rollback
记录 {key_startTS
} 与其他事务的提交记录 {key_commitTS
} 重叠 (可能t1
的startTS
恰好是t2
事务的commitTS
),那么一般情况下可以省略rollback
记录的写入,为集群减少负担。但是如果
KEY
是悲观事务的Primary KEY
的话,就需要将提交记录{key_commitTS
} 设置一个overlapped_rollback
标记删除
LOCK
记录如果没有发现锁或者发现的锁并不是本事务的,而是其他事务的
LOCK
,那么需要调用check_txn_status_missing_lock
如果发现有本事务的
OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明已经回滚完成,直接返回OK
即可终止回滚流程如果发现有本事务提交记录的话,返回
ErrorInner::Committed
如果没有找到任何本事务
write
记录的话如果发现了其他事务的锁:首先需要调用
mark_rollback_on_mismatching_lock
在这个LOCK
上面添加回滚LockTS
标记,这样这个lock
所涉及的事务在提交后,如果发现自己的commitTS
和LockTS
重叠的话,需要设置一下overlap
标记保护模式下调用
make_rollback
写入rollback
记录,确保这个回滚记录不会被删除删除
collapse
以前的非保护rollback
记录
rollback_lock
为了保险起见,会再次通过
get_txn_commit_record
函数查看write
的最新记录如果发现
write
上有当前事务的提交记录,直接panic
如果发现有
OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明之前已经添加了write
回滚记录,删除了default
上面的value
数据,那么现在只需要把LOCK
记录删除即可如果没有发现任何提交记录或者回滚记录,那么
如果
LOCK
是PUT
类型、且已经写入default CF Value
,那么需要删除default CF Value
非保护模式下利用
make_rollback
生成rollback
类型的write
记录特别需要注意的是,由于是非保护模式下,所以如果恰好
rollback
记录 {key_startTS
} 与其他事务的提交记录 {key_commitTS
} 重叠 (可能t1
的startTS
恰好是t2
事务的commitTS
),那么一般情况下可以省略rollback
记录的写入,为集群减少负担。但是如果
KEY
是悲观事务的Primary KEY
的话,就需要将提交记录{key_commitTS
} 设置一个overlapped_rollback
标记删除
collapse
以前的非保护rollback
记录删除
LOCK
记录
check_txn_status_missing_lock
如果发现有本事务的
OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明已经回滚完成,直接返回OK
即可终止回滚流程如果发现有本事务提交记录的话,返回
ErrorInner::Committed
如果没有找到任何本事务
write
记录的话 (这个场景可能比较少见)首先需要调用
mark_rollback_on_mismatching_lock
在这个LOCK
上面添加回滚LockTS
标记,这样这个lock
所涉及的事务在提交后,如果发现自己的commitTS
和LockTS
重叠的话,需要设置一下overlap
标记保护模式下调用
make_rollback
写入rollback
记录,确保这个回滚记录不会被删除删除
collapse
以前的非保护rollback
记录
Cleanup
Cleanup
和 Rollback
实际上调用的代码区别不大,关键点就是调用 action::cleanup
函数的时候,传递的 protect_rollback
参数是 true
,也就是说 Cleanup
接口的回滚记录全部都是保护模式的。
Cleanup
比较重要的作用就是清理当前事务中,已经不需要的锁信息。因此,为了保险起见 ,Cleanup
接口会留下保护类型的回滚记录,防止网络异常原因导致的 stale
prewrite
请求,并且请求成功导致事务被错误提交。
关于何时调用 Cleanup
何时调用 Rollback
,需要具体看 tikv-client
的逻辑甚至看 TIDB
的逻辑,目前笔者对此了解不多。只能从 TIKV
的代码来猜测,Rollback
应该是用于非常确定的场景,即使出现了当前事务的 stale
prewrite
请求,也不会导致事务会被成功提交,因此其回滚记录可以是非保护模式的,即使被删除了也无所谓。其他场景都是需要 Cleanup
接口,把回滚记录保护起来,拦截阻止 stale
prewrite
请求的成功。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/da29d66b914b519bffb7e2346】。文章转载请联系作者。
评论