黄东旭解析 TiDB 的核心优势
462
2024-03-14
上一篇介绍了 Prewrite 接口,这篇我们继续介绍 Commit/Rollback
接口,Cleanup
接口实际上和 Rollback
接口类似。
除此之外,还有 CheckTxnStatus
/ ResolveLock
/ CheckSecondaryLocks
关键接口,由于篇幅有限,只能后面有机会再聊
KEYS
: Commit
提交的涉及的 KEYS,相关的 KEYS
和 Prewrite
相同
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
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
可能是因为 pessimistic rollback
请求未能发送到 TIKV
,也可能是 TIKV
由于某种情况下突然收到了pessimistic lock
请求,个人理解这些特殊场景可能并不是二阶段过程中会发生的,因为 Prewrite
成功后不可能将常规的锁转为悲观锁,根据注释大概率应该是 resolve lock
过程中可能遇到的场景
如果 LOCK
类型是正常的锁,那么删除锁,并且添加新的 write
记录即可正常返回
如果 LOCK
类型是悲观锁,这个是非预期的,这时候 commit
操作实际上就是删除悲观锁即可 (并不需要 write CF
上的回滚记录)。
还有一种比较特殊的情况,那就是存在并发的两个事务,t1
与 t2
,t1
开启的时间很早,也就是 t1.start_ts < t2.start_ts
fn process_write(self, snapshot: S, context: WriteContext<'_, L>) -> Result<WriteResult> { if self.commit_ts <= self.lock_ts { return Err(Error::from(ErrorInner::InvalidTxnTso... } ... for k in self.keys { released_locks.push(commit(&mut txn, &mut reader, k, self.commit_ts)?); } let mut write_data = WriteData::from_modifies(txn.into_modifies()); Ok(WriteResult {...}) pub fn commit<S: Snapshot>( txn: &mut MvccTxn, reader: &mut SnapshotReader<S>, key: Key, commit_ts: TimeStamp, ) -> MvccResult<Option<ReleasedLock>> { let (mut lock, commit) = match reader.load_lock(&key)? { Some(lock) if lock.ts == reader.start_ts => { if commit_ts < lock.min_commit_ts { return Err(ErrorInner::CommitTsExpired... } if lock.is_pessimistic_lock() { (lock, false) } else { (lock, true) } } _ => { return match reader.get_txn_commit_record(&key)?.info() { Some((_, WriteType::Rollback)) | None => { Err(ErrorInner::TxnLockNotFound... } Some((_, WriteType::Put)) | Some((_, WriteType::Delete)) | Some((_, WriteType::Lock)) => { Ok(None) } }; } }; if !commit { // Rollback a stale pessimistic lock. This function must be called by // resolve-lock in this case. assert!(lock.is_pessimistic_lock()); return Ok(txn.unlock_key(key, lock.is_pessimistic_txn(), TimeStamp::zero())); } let mut write = Write::new( WriteType::from_lock_type(lock.lock_type).unwrap(), reader.start_ts, lock.short_value.take(), )... for ts in &lock.rollback_ts { if *ts == commit_ts { write = write.set_overlapped_rollback(true, None); break; } } txn.put_write(key.clone(), commit_ts, write.as_ref().to_bytes()); Ok(txn.unlock_key(key, lock.is_pessimistic_txn(), commit_ts))} pub(crate) fn unlock_key( &mut self, key: Key, pessimistic: bool, commit_ts: TimeStamp, ) -> Option<ReleasedLock> { let released = ReleasedLock::new(self.start_ts, commit_ts, key.clone(), pessimistic); let write = Modify::Delete(CF_LOCK, key); self.write_size += write.size(); self.modifies.push(write); Some(released) } get_txn_commit_record:扫描从 max_ts 到 t1.start_ts 之间 key 的 write record 来判断 t1 状态 return TxnCommitRecord::SingleRecord: 找到了 write.start_ts = t1.ts1 的,WriteRecord 可能是回滚记录,也可能是提交记录 return TxnCommitRecord::OverlappedRollback: 找到了 t1.start_ts == t3.commit_ts, 而且 has_overlapped_write 是 true return TxnCommitRecord::None(Some(OverlappedWrite)): 找到了 t1.start_ts == t3.commit_ts, 而且 has_overlapped_write 为 false。实际上该记录和 rollback 记录重叠了,需要设置 has_overlapped_write return TxnCommitRecord::None: 没找到 t1 的 commit 记录 pub fn info(&self) -> Option<(TimeStamp, WriteType)> { match self { Self::None { .. } => None, Self::SingleRecord { commit_ts, write } => Some((*commit_ts, write.write_type)), Self::OverlappedRollback { commit_ts } => Some((*commit_ts, WriteType::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
mysql> begin optimistic; Query OK, 0 rows affected (0.00 sec) mysql> DELETE FROM MANAGERS_UNIQUE where FIRST_NAME='Brad7'; Query OK, 1 row affected (0.00 sec) mysql> commit; ERROR 9007 (HY000): Write conflict, txnStartTS=448235833448988673, conflictStartTS=448235835493974020, conflictCommitTS=448235837853270018, key={tableID=106, tableName=test.MANAGERS_UNIQUE, indexID=2, indexValues={Brad7, }}, originalKey=74800000000000006a5f698000000000000002014272616437000000fc, primary={tableID=106, tableName=test.MANAGERS_UNIQUE, indexID=2, indexValues={Brad7, }}, originalPrimaryKey=74800000000000006a5f698000000000000002014272616437000000fc, reason=Optimistic [try again later sched_txn_command kv::command::rollback keys([ 7480000000000000FF6A5F698000000000FF0000020142726164FF37000000FC000000FC ]) @ start_ts:448235833448988673 | 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 sched_txn_command kv::command::rollback keys([ 7480000000000000FF6A5F698000000000FF0000030380000000FF0000000701313432FF3733000000FC0000FD, 7480000000000000FF6A5F720131343237FF33000000FC000000FC ]) @ start_ts:448235833448988673 | 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: 0A209505CACB7C710ED17125FCC6CB3669E8DDCA6C8CD8AF6A31F6B3CD64604C30981802 request_source: "external_Commit" resource_control_context { resource_group_name: "default" penalty {} override_priority: 8 } keyspace_id: 4294967295
我们知道回滚记录是个比较特殊的 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
进行回滚操作
如果发现 write
上有当前事务的提交记录,直接 panic
如果发现有 OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明之前已经添加了 write
回滚记录,删除了 default
上面的 value
数据,那么现在只需要把 LOCK
记录删除即可
如果没有发现任何提交记录或者回滚记录,那么
特别需要注意的是,由于是非保护模式下,所以如果恰好 rollback
记录 {key_startTS
} 与其他事务的提交记录 {key_commitTS
} 重叠 (可能 t1
的 startTS
恰好是 t2
事务的 commitTS
),那么一般情况下可以省略 rollback
记录的写入,为集群减少负担。
但是如果 KEY
是悲观事务的 Primary KEY
的话,就需要将提交记录{key_commitTS
} 设置一个 overlapped_rollback
标记
如果 LOCK
是 PUT
类型、且已经写入 default CF Value
,那么需要删除 default CF Value
非保护模式下利用 make_rollback
生成 rollback
类型的 write
记录
删除 LOCK
记录
rollback_lock
为了保险起见,会再次通过 get_txn_commit_record
函数查看 write
的最新记录
如果没有发现锁或者发现的锁并不是本事务的,而是其他事务的 LOCK
,那么需要调用 check_txn_status_missing_lock
如果发现了其他事务的锁:首先需要调用 mark_rollback_on_mismatching_lock
在这个 LOCK
上面添加回滚 LockTS
标记,这样这个 lock
所涉及的事务在提交后,如果发现自己的 commitTS
和 LockTS
重叠的话,需要设置一下 overlap
标记
保护模式下调用 make_rollback
写入 rollback
记录,确保这个回滚记录不会被删除
删除 collapse
以前的非保护rollback
记录
如果发现有本事务的 OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明已经回滚完成,直接返回 OK
即可终止回滚流程
如果发现有本事务提交记录的话,返回 ErrorInner::Committed
如果没有找到任何本事务 write
记录的话
fn process_write(self, snapshot: S, context: WriteContext<'_, L>) -> Result<WriteResult> { ... let rows = self.keys.len(); let mut released_locks = ReleasedLocks::new(); for k in self.keys { // Rollback is called only if the transaction is known to fail. Under the // circumstances, the rollback record needn't be protected. let released_lock = cleanup(&mut txn, &mut reader, k, TimeStamp::zero(), false)?; released_locks.push(released_lock); } let mut write_data = WriteData::from_modifies(txn.into_modifies()); Ok(WriteResult { ...} pub fn cleanup<S: Snapshot>( txn: &mut MvccTxn, reader: &mut SnapshotReader<S>, key: Key, current_ts: TimeStamp, protect_rollback: bool, ) -> MvccResult<Option<ReleasedLock>> { match reader.load_lock(&key)? { Some(ref lock) if lock.ts == reader.start_ts => { ... rollback_lock( txn, reader, key, lock, lock.is_pessimistic_txn(), !protect_rollback, ) } l => match check_txn_status_missing_lock( txn, reader, key.clone(), l, MissingLockAction::rollback_protect(protect_rollback), false, )? { TxnStatus::Committed { commit_ts } => { Err(ErrorInner::Committed... } TxnStatus::RolledBack => { Ok(None) } TxnStatus::LockNotExist => Ok(None), _ => unreachable!(), }, } }
为了保险起见,会再次通过 get_txn_commit_record
函数查看 write
的最新记录
如果 LOCK
是 PUT
类型、且已经写入 default CF Value
,那么需要删除 default CF Value
非保护模式下利用 make_rollback
生成 rollback
类型的 write
记录
删除 collapse
以前的非保护rollback
记录
删除 LOCK
记录
特别需要注意的是,由于是非保护模式下,所以如果恰好 rollback
记录 {key_startTS
} 与其他事务的提交记录 {key_commitTS
} 重叠 (可能 t1
的 startTS
恰好是 t2
事务的 commitTS
),那么一般情况下可以省略 rollback
记录的写入,为集群减少负担。
但是如果 KEY
是悲观事务的 Primary KEY
的话,就需要将提交记录{key_commitTS
} 设置一个 overlapped_rollback
标记
如果发现 write
上有当前事务的提交记录,直接 panic
如果发现有 OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明之前已经添加了 write
回滚记录,删除了 default
上面的 value
数据,那么现在只需要把 LOCK
记录删除即可
如果没有发现任何提交记录或者回滚记录,那么
pub fn rollback_lock( txn: &mut MvccTxn, reader: &mut SnapshotReader<impl Snapshot>, key: Key, lock: &Lock, is_pessimistic_txn: bool, collapse_rollback: bool, ) -> Result<Option<ReleasedLock>> { let overlapped_write = match reader.get_txn_commit_record(&key)? { TxnCommitRecord::None { overlapped_write } => overlapped_write, TxnCommitRecord::SingleRecord { write, commit_ts } if write.write_type != WriteType::Rollback => { panic!( ... } _ => return Ok(txn.unlock_key(key, is_pessimistic_txn, TimeStamp::zero())), }; // If prewrite type is DEL or LOCK or PESSIMISTIC, it is no need to delete // value. if lock.short_value.is_none() && lock.lock_type == LockType::Put { txn.delete_value(key.clone(), lock.ts); } // Only the primary key of a pessimistic transaction needs to be protected. let protected: bool = is_pessimistic_txn && key.is_encoded_from(&lock.primary); if let Some(write) = make_rollback(reader.start_ts, protected, overlapped_write) { txn.put_write(key.clone(), reader.start_ts, write.as_ref().to_bytes()); } if collapse_rollback { collapse_prev_rollback(txn, reader, &key)?; } Ok(txn.unlock_key(key, is_pessimistic_txn, TimeStamp::zero()))} pub fn make_rollback( start_ts: TimeStamp, protected: bool, overlapped_write: Option<OverlappedWrite>, ) -> Option<Write> { match overlapped_write { Some(OverlappedWrite { write, gc_fence }) => { assert!(start_ts > write.start_ts); if protected { Some(write.set_overlapped_rollback(true, Some(gc_fence))) } else { // No need to update the original write. None } } None => Some(Write::new_rollback(start_ts, protected)), } }
如果发现有本事务的 OverlappedRollback
的记录或者回滚记录 (SingleRecord::Rollback
),说明已经回滚完成,直接返回 OK
即可终止回滚流程
如果发现有本事务提交记录的话,返回 ErrorInner::Committed
如果没有找到任何本事务 write
记录的话 (这个场景可能比较少见)
首先需要调用 mark_rollback_on_mismatching_lock
在这个 LOCK
上面添加回滚 LockTS
标记,这样这个 lock
所涉及的事务在提交后,如果发现自己的 commitTS
和 LockTS
重叠的话,需要设置一下 overlap
标记
保护模式下调用 make_rollback
写入 rollback
记录,确保这个回滚记录不会被删除
删除 collapse
以前的非保护rollback
记录
pub fn check_txn_status_missing_lock( txn: &mut MvccTxn, reader: &mut SnapshotReader<impl Snapshot>, primary_key: Key, mismatch_lock: Option<Lock>, action: MissingLockAction, resolving_pessimistic_lock: bool, ) -> Result<TxnStatus> { match reader.get_txn_commit_record(&primary_key)? { TxnCommitRecord::SingleRecord { commit_ts, write } => { if write.write_type == WriteType::Rollback { Ok(TxnStatus::RolledBack) } else { Ok(TxnStatus::committed(commit_ts)) } } TxnCommitRecord::OverlappedRollback { .. } => Ok(TxnStatus::RolledBack), TxnCommitRecord::None { overlapped_write } => { ... let ts = reader.start_ts; // collapse previous rollback if exist. if action.collapse_rollback() { collapse_prev_rollback(txn, reader, &primary_key)?; } if let (Some(l), None) = (mismatch_lock, overlapped_write.as_ref()) { txn.mark_rollback_on_mismatching_lock( &primary_key, l, action == MissingLockAction::ProtectedRollback, ); } // Insert a Rollback to Write CF in case that a stale prewrite // command is received after a cleanup command. if let Some(write) = action.construct_write(ts, overlapped_write) { txn.put_write(primary_key, ts, write.as_ref().to_bytes()); } Ok(TxnStatus::LockNotExist) } }}
Cleanup
和 Rollback
实际上调用的代码区别不大,关键点就是调用 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 { ... })
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。