TiDB是如何保证delete range场景下不误删还有可能写入的range的?

想到一个corner case,看了tidb的代码没找到相关的特殊处理,很好奇。

假设有一个写事务T,以及GC leader。

我们知道一个乐观事务的执行顺序是:

  1. 获取start ts
  2. 在tidb内存里做读写操作
  3. 并发prewrite primary key与secondary keys
  4. 获取commit ts
  5. commit primary key
  6. commit secondary keys

而gc leader的执行顺序是

  1. 计算safe point:min(当前时间 - gc_life_time, 各个tidb节点上的活跃事务的start_ts最小值)
  2. 执行scan locks请求
  3. 执行resolve locks请求
  4. 执行delete range
  5. 清理mvcc

假设用户drop了某个index idx,online ddl已经推进到none状态,但T的schema还是delete only状态。

是否存在这样一个时序:
0. T选择idx对应的range里的某个key为primary key

  1. T的prewrite primary & secondary请求卡在网络上超时了,session被干掉,因此T.start_ts不会被算到safe point里
  2. gc leader计算出safe point为 当前时间 - gc_life_time
  3. gc leader执行了scan locks,显然扫不到步骤1里on-the-fly的secondary锁
  4. gc leader执行了resolve locks,显然也不会处理步骤1里的锁
  5. T的prewrite请求到达tikv,加锁成功
  6. gc leader将idx对应的range删除,即primary key被删除
  7. resolve secondary key时发现找不到primary key,不知道应该提交还是回滚

MVCC 就够拉,

删除的是一个版本,写入的是另外一个版本 ,没冲突…

感谢回复,但我说的场景是delete range,所有版本都会被删除

事务一致性?

不论删除还是写入,不考虑事务问题,不考虑锁,怎么样能保证你说的场景能实现?

mysql 都可以,为啥 tidb 不可以呢… :upside_down_face:

我描述的场景是用户发起drop index后,tidb后台执行delete range,此时有一个并发的事务T,事务T的时序也符合tidb目前的实现。如果你觉得不可能发生麻烦指出哪一步有问题,而不是在这阴阳怪气。

能麻烦说的具体点吗?

drop index → DDL ?
delete range data → DML ?

不好意思,能在具体点么? ps: 哪儿阴阳怪气了?

drop index是ddl,delete range是ddl底层对应的实现,不是dml。参考https://docs.pingcap.com/zh/tidb/stable/garbage-collection-overview 中描述的““Delete Ranges” 阶段快速地删除由于 DROP TABLE/DROP INDEX 等操作产生的整区间的废弃数据。”
我举的例子里DML是另一个普通的写事务T,它的schema可能比当前schema旧1个version

sorry, :upside_down_face: 这个表情看着不太舒服

那我知道你想了解什么了,就是 F1 的实现机制
这个老复杂了,推荐给你一些文档,可以慢慢看

另外tidb 有课程对这些内容有更详细的描述,可以去直接学习
https://learn.pingcap.com/learner/course/750002

包老师讲得还是很细的


不论是 DDL 还是 DML,实际上操纵的基础还是 region,在 tikv 层面来看就是 kv 模式
所以数据的处理过程是一样的,但是对于版本的管理和介入机制却不同… 只能说十分复杂…

F1的原理和实现我是了解的,但感觉例子里举的这个corner case时序是有可能发生的

这个就需要用实例来证明了,比较困难

所有的场景都是在假设之上,讨论这个就更难了…

好吧,还是谢谢了~

根据描述的情况,的确存在一种时序,可能会导致问题,特别是当涉及在线 DDL 操作和乐观事务的同时进行时。在线 DDL 操作可能会导致索引删除,而正在进行的事务可能会在乐观锁定过程中涉及到这些索引。这可能会导致 GC Leader 在清理事务时遇到问题。

要解决这个问题,可以考虑在 GC Leader 的处理逻辑中增加一些特殊情况的处理。以下是一种可能的解决方案的思路:

在 GC Leader 计算 safe point 时,除了计算 min(当前时间 - gc_life_time, 各个tidb节点上的活跃事务的start_ts最小值) 外,还要考虑正在进行的在线 DDL 操作的情况。

当 GC Leader 执行 scan locks 时,需要识别涉及到在线 DDL 操作的锁,并确保这些锁在 safe point 之前被扫描和处理。这可能需要维护在线 DDL 操作的元信息以及它们的相关锁信息。

当 GC Leader 执行 resolve locks 时,需要处理在线 DDL 操作的锁,即使它们的 primary key 已经被删除。这可能需要特殊逻辑来判断应该如何处理这些锁,例如提交或回滚。

确保 GC Leader 在清理过程中不会忽略与在线 DDL 相关的锁。

2 个赞

https://docs.pingcap.com/zh/tidb/stable/garbage-collection-overview#resolve-locks清理锁

### Resolve Locks(清理锁)

TiDB 的事务是基于 [Google Percolator](https://ai.google/research/pubs/pub36726) 模型实现的,事务的提交是一个两阶段提交的过程。第一阶段完成时,所有涉及的 key 都会上锁,其中一个锁会被选为 Primary,其余的锁 (Secondary) 则会存储一个指向 Primary 的指针;第二阶段会将 Primary 锁所在的 key 加上一个 Write 记录,并去除锁。这里的 Write 记录就是历史上对该 key 进行写入或删除,或者该 key 上发生事务回滚的记录。Primary 锁被替换为何种 Write 记录标志着该事务提交成功与否。接下来,所有 Secondary 锁也会被依次替换。如果因为某些原因(如发生故障等),这些 Secondary 锁没有完成替换、残留了下来,那么也可以根据锁中的信息取找到 Primary,并根据 Primary 是否提交来判断整个事务是否提交。但是,如果 Primary 的信息在 GC 中被删除了,而该事务又存在未成功提交的 Secondary 锁,那么就永远无法得知该锁是否可以提交。这样,数据的正确性就无法保证。

Resolve Locks 这一步的任务即对 safe point 之前的锁进行清理。即如果一个锁对应的 Primary 已经提交,那么该锁也应该被提交;反之,则应该回滚。而如果 Primary 仍然是上锁的状态(没有提交也没有回滚),则应当将该事务视为超时失败而回滚。

关于清理锁的描述是这样的,我觉得完全可以回答你的问题。

其实就是看Primary 里面是否有commit_ts,没有的情况下就是没有完成的事务。将会被清理。

或者我感觉可以翻译一下你的问题,即,一个大的乐观事务如果在gc间隔时间内无法完成,是否就永远完成不了。
我觉得答案是肯定的。以默认gc时间10分钟为例,如果一个大乐观事务,就是运行10分钟也完不成,不修改gc间隔的情况下,那就永远也无法完成了。这也解释了为什么lightning的大量数据导入/tispark长时间的olap计算,tidb的组件都会停掉safepoint的推进。

他是想证明他的猜想和假设,,拿tidb 论证这个过程 :slightly_smiling_face:

我觉得他想证明,或者有更简单的方式。

有代码的情况下,可以调试起来,在某些地方直接修改代码sleep一下。可以复现任何场景。
讨论确实有描述或者理解不一致的情况。

async commit
async commit 是一种优化 TiDB 事务性能的特性,它可以在 prewrite 阶段就返回事务成功的结果,而不需要等待 commit 阶段完成。这样,就可以减少事务的延迟,提高吞吐量。

async commit 的原理是,在 prewrite 阶段,TiDB 会为每个 key 生成一个 commit ts,并将其写入锁中。这个 commit ts 由当前的 start ts 和最大 ts 中的较大值决定,同时也满足 commit ts > start ts 的条件。

当 prewrite 阶段完成后,TiDB 会从所有 key 的锁中选择一个最小的 commit ts 作为当前事务的 commit ts,并将其返回给客户端。客户端收到 commit ts 后,就可以认为事务已经成功提交了。

在后台,TiDB 会继续执行 commit 阶段,将所有 key 的锁标记为已提交,并使用各自的 commit ts 更新 write cf。如果某个 key 的锁被其他事务发现了,那么其他事务就可以根据锁中的信息判断该 key 是否已经提交,并使用相同的 commit ts 读取该 key 的值。

通过 async commit,TiDB 可以保证事务的快照隔离级别(SI),并且可以避免一些由网络延迟或者 GC 导致的异常情况。例如,在你提出的 corner case 中,由于 T 的 primary key 已经被 GC 删除了,所以 resolve secondary key 时就无法找到 primary key 的信息。但是如果使用了 async commit,那么 T 的 secondary key 就可以从自己的锁中获取 commit ts,并且可以正确地提交或回滚。

1 个赞