写点什么

TIKV 源码学习笔记 -- 分布式事务接口 Commit/Rollback

  • 2024-03-15
    北京
  • 本文字数:5560 字

    阅读完需:约 18 分钟

作者: ylldty 原文来源:https://tidb.net/blog/2123ce5d

前言

上一篇介绍了 Prewrite 接口,这篇我们继续介绍 Commit/Rollback 接口,Cleanup 接口实际上和 Rollback 接口类似。


除此之外,还有 CheckTxnStatus/ ResolveLock / CheckSecondaryLocks 关键接口,由于篇幅有限,只能后面有机会再聊

Commit

参数

  • KEYS: Commit 提交的涉及的 KEYS,相关的 KEYSPrewrite 相同

  • LOCK_TS: Commit 需要消除的 LOCK TS,一般也是事务的 start_ts

  • COMMIT_TS: 提交的最终 commit_ts


UPDATE 语句为例:


UPDATE MANAGERS_UNIQUE SET FIRST_NAME="Brad9" where FIRST_NAME='Brad10';
sched_txn_command kv::command::commit [ 7480000000000000FF6A5F720131343237FF36000000FC000000FC] start_ts:448099651396042753 -> commit_ts:448099662328233986 | region_id: 14 region_epoch { conf_ver: 1 version: 61 } peer { id: 15 store_id: 1 } isolation_level: Si max_execution_duration_ms: 20000 resource_group_tag: 0A209505CACB7C710ED17125FCC6CB3669E8DDCA6C8CD8AF6A31F6B3CD64604C30981801 request_source: "external_Commit" resource_control_context { resource_group_name: "default" penalty {} override_priority: 8 } keyspace_id: 4294967295

sched_txn_command kv::command::commit [ 7480000000000000FF6A5F698000000000FF0000020142726164FF31300000FD000000FC, 7480000000000000FF6A5F698000000000FF0000020142726164FF39000000FC000000FC] start_ts:448099651396042753 -> commit_ts:448099662328233986 | region_id: 129 region_epoch { conf_ver: 1 version: 61 } peer { id: 130 store_id: 1 } isolation_level: Si max_execution_duration_ms: 20000 resource_group_tag: 0A209505CACB7C710ED17125FCC6CB3669E8DDCA6C8CD8AF6A31F6B3CD64604C30981802 request_source: "external_Commit" resource_control_context { resource_group_name: "default" penalty {} override_priority: 8 } keyspace_id: 4294967295
复制代码


其他语句类似,区别不大,这里不在赘述。

代码简读

  • 对每个 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 过程中可能遇到的场景

  • 还有一种比较特殊的情况,那就是存在并发的两个事务,t1t2t1 开启的时间很早,也就是 t1.start_ts < t2.start_ts

  • t1KEY 调用了 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_rollbacktrue,标志这个 write 记录其实是叠加了两个事务的 commitrollback

Rollback

场景

和直观认知可能不太一样,TIKVRollback 接口一般情况下并不是 sqlrollback 语句触发的。


对于乐观事务来说,由于事务过程中,没有加任何锁,因此 sql rollback 语句实际上并不需要调用 tikv 的接口处理,只需要将 Buffput 数据清空即可。


对于悲观事务来说,事务过程中加了悲观锁,但是 sql rollback 语句实际触发的是 pessimistic_rollback 这个接口,专门用于清理悲观锁。


TIKVRollback 接口常见于乐观事务写冲突的时候,乐观事务在进行二阶段提交过程中,prewrite 过程中发现了写冲突,这时候就需要调用 TIKVRollback


  • 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,相关的 KEYSPrewrite 相同

  • LOCK_TS: Commit 需要消除的 LOCK TS,一般也是事务的 start_ts

Overlapped Rollback 回滚记录

我们知道回滚记录是个比较特殊的 write 记录,不仅仅是 write.typerollback 类型的,而且还因为其 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,提交记录的 KEYKEY_commit_t1

  • T2 事务启动 startTS=start_t2,然后被回滚,因此其回滚记录的 KEYKEY_start_t2

  • 由于 commit_t1 并不是 PD 获取的,而 start_t2 是 PD 获取的 ts,因此就有概率 commit_t1==start_t2,也就是说两个事务的提交记录和回滚记录在 write CF 上重叠了

  • 这个时候,就需要一个属性值 Overlapped,当一个提交记录的 Overlappedtrue 的时候,就代表这其实是两个记录,一个提交记录一个回滚记录

保护模式和非保护模式回滚记录

当我们事务冲突很严重的时候,就容易有多条的回滚记录,这对于 TIKVmvcc 扫描来说效率太慢了。因此 TIKV 有个优化,在 write CF 上面,对于一个 KEY,只保留最新的那个回滚记录即可,其他回滚记录可以直接删除。


但是为了正确性考虑,必须防止已经对 KEY 进行了回滚操作,后面突然由于网络原因又出现对 KEY 调用了 prewritecommit,导致回滚的事件被错误的提交。


因此只能对部分 KEY 进行这种 collapse 删除优化。


具体的就是对于 rowID、唯一索引来说,采用保护模式的回滚,该回滚记录不会被删除。这样每次事务被错误的 commit 的时候,都可以通过被保护的回滚记录了解到这个事务实际上已经被提交了。


对于普通索引,采用非保护模式,可能被其他事务更新的 rollback 记录删除,也可能遇到需要 Overlapped 的场景,并不设置 Overlappedtrue


最后实际上最后结果就是:普通索引上面即使被回滚了,但是却找不到任何回滚的记录。

代码简读

  • 对每个 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 记录删除即可

  • 如果没有发现任何提交记录或者回滚记录,那么

  • 如果 LOCKPUT 类型、且已经写入 default CF Value ,那么需要删除 default CF Value

  • 非保护模式下利用 make_rollback 生成 rollback 类型的 write 记录

  • 特别需要注意的是,由于是非保护模式下,所以如果恰好 rollback 记录 {key_startTS} 与其他事务的提交记录 {key_commitTS} 重叠 (可能 t1startTS 恰好是 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 所涉及的事务在提交后,如果发现自己的 commitTSLockTS 重叠的话,需要设置一下 overlap 标记

  • 保护模式下调用 make_rollback 写入 rollback 记录,确保这个回滚记录不会被删除

  • 删除 collapse 以前的非保护rollback 记录

rollback_lock

  • 为了保险起见,会再次通过 get_txn_commit_record 函数查看 write 的最新记录

  • 如果发现 write 上有当前事务的提交记录,直接 panic

  • 如果发现有 OverlappedRollback 的记录或者回滚记录 (SingleRecord::Rollback),说明之前已经添加了 write 回滚记录,删除了 default 上面的 value 数据,那么现在只需要把 LOCK 记录删除即可

  • 如果没有发现任何提交记录或者回滚记录,那么

  • 如果 LOCKPUT 类型、且已经写入 default CF Value ,那么需要删除 default CF Value

  • 非保护模式下利用 make_rollback 生成 rollback 类型的 write 记录

  • 特别需要注意的是,由于是非保护模式下,所以如果恰好 rollback 记录 {key_startTS} 与其他事务的提交记录 {key_commitTS} 重叠 (可能 t1startTS 恰好是 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 所涉及的事务在提交后,如果发现自己的 commitTSLockTS 重叠的话,需要设置一下 overlap 标记

  • 保护模式下调用 make_rollback 写入 rollback 记录,确保这个回滚记录不会被删除

  • 删除 collapse 以前的非保护rollback 记录

Cleanup

CleanupRollback 实际上调用的代码区别不大,关键点就是调用 action::cleanup 函数的时候,传递的 protect_rollback 参数是 true,也就是说 Cleanup 接口的回滚记录全部都是保护模式的。


Cleanup 比较重要的作用就是清理当前事务中,已经不需要的锁信息。因此,为了保险起见 ,Cleanup 接口会留下保护类型的回滚记录,防止网络异常原因导致的 stale prewrite 请求,并且请求成功导致事务被错误提交。


关于何时调用 Cleanup 何时调用 Rollback ,需要具体看 tikv-client 的逻辑甚至看 TIDB 的逻辑,目前笔者对此了解不多。只能从 TIKV 的代码来猜测,Rollback 应该是用于非常确定的场景,即使出现了当前事务的 stale prewrite 请求,也不会导致事务会被成功提交,因此其回滚记录可以是非保护模式的,即使被删除了也无所谓。其他场景都是需要 Cleanup 接口,把回滚记录保护起来,拦截阻止 stale prewrite 请求的成功。


fn process_write(self, snapshot: S, context: WriteContext<'_, L>) -> Result<WriteResult> {        // It is not allowed for commit to overwrite a protected rollback. So we update        // max_ts to prevent this case from happening.        context.concurrency_manager.update_max_ts(self.start_ts);        ...
let mut released_locks = ReleasedLocks::new(); released_locks.push(cleanup( &mut txn, &mut reader, self.key, self.current_ts, true, )?);
let new_acquired_locks = txn.take_new_locks(); let mut write_data = WriteData::from_modifies(txn.into_modifies()); Ok(WriteResult { ... })
复制代码


发布于: 刚刚阅读数: 3
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
TIKV 源码学习笔记--分布式事务接口 Commit/Rollback_TiDB 底层架构_TiDB 社区干货传送门_InfoQ写作社区