关于TiDB 2PC中commit 阶段疑问

TiDB 在commit 阶段会清锁并写入版本数据, 这两个操作能保证原子的吗?

TiDB 编码大致如下:

Data column
${key}${start_ts} —> ${value}
version column:
${key}
${commit_ts} → ${start_ts}
lock column
${key}–>${start_ts,primary_key,…etc}

commit 阶段,primary 会将lock column 的锁信息清掉并将commit_ts写入version column, 这应该是两个不同的row, 对于rocksdb来说并不能保证这两个操作的原子性(两行可能落在不同的region上, region内应该是可以保证原子性的, 跨region应该不能), 哪位大佬能帮忙解惑一下

可以,因为锁是任意选一行加 primary lock,其他锁都是指向锁,相当于一个定向。主锁清理之后,其他指向锁异步清理。查的时候会确认指向锁 指向的 lock 是否存在,所以跨 region、节点没问题。

1 个赞

primary 会将lock column 的锁信息清掉并将commit_ts写入version column

清理锁与写version column 这个由于不是同一个row, 不能确保会落在同一个region上, 不能保证原子性吧?
我是哪里的上下文没有get 到么

其他行上面加的是指向锁,可以的。

大佬, 我说的是commit 阶段修改primary的lock的commit_ts 和 version column。

关于指向锁我明白, 只要primary 修改成功了, 事务就成功了, secondarys 可以异步完成, 即使失败了也没有关系, 有指向primary的标记

Percolator 中 lock列和version列信息共享同一个row_key, 对同一行的不同cf修改确实是原子的。 TiDB上面我看
lock列与version列用的是不同的row_key, 怎样能保证是原子的?

两阶段提交2PC

RocksDB Column Family 简称:
D 列:rocksdb.defaultcf
L 列:rocksdb.lockcf
W 列:rocksdb.writecf

事务提交前,会在 TiDB 缓存所有数据。这边有遇到过同时并发写入上百MB数据,结果导致 TiDB OOM。
1、TiDB 从当前要写入的行中选择一个 Key 作为当前事务的 Primary Key,剩于的 Keys 为 secondary
2、TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照路由进行分类
3、TiDB 发起 prewrite 请求,将Primary Key与数据写入到 TiKV,并进行加锁【加锁前会检查是否有写入冲突】,

加锁成功后执行下面操作:
(1) 锁信息写入L列,示例:<1,(W,pk,1,100 … )>
(2) 行数据写入D列,示例:put<1_100,‘相亲相爱一家人’>

4、然后 Secondary Key 并发地向所有涉及的 TiKV 发起 prewrite 请求,流程同 Primary Key 类似
区别是锁信息指向了 Primary Key :
(1) 锁信息写入L列,示例:<2,(W,@1,2,100 … )>

5、TiDB 收到所有 prewrite 都成功

6、TiDB 向 PD 获取 commit_ts

7、TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交的 Commit
(1) 写入 W 列,示例:put<1_110,100>
(2) 删除 L 列,示例:<1,(D,pk,1,100 … )>
(3) 最后清理锁信息

8、Primary Commit 提交成功后,Secondary 可以进行异步提交

9、TiDB 收到两阶段提交成功
【加锁前会检查是否有写入冲突】

  • 检查 L 列,是否已经有别的客户端已经上锁 (Locking)
  • 检查 W 列,在本次事务开始时间之后,是否有更新 [startTs, +Inf) 的写操作已经提交 (Conflict)

Prewrite 出现冲突,当前事务回滚。
Primary Commit 出现冲突,全事务回滚。

7、TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交的 Commit
(1) 写入 W 列,示例:put<1_110,100>
(2) 删除 L 列,示例:<1,(D,pk,1,100 … )>
(3) 最后清理锁信息

整个第7步是原子的吗? (1)成功了(2) 失败了怎么办?还是说整个1,2,3是一个原子操作, 那这个原子操作如何实现? W、L列的row_key不一样, 不能保证落到同一个region中吧?

  • 在修改数据由内存写入TiKV节点时,TiKV节点使用三个列簇(CF)来分别存放修改数据(Default),锁信息(Lock),提交信息(Write)。
  • 写入时,在Default列簇里,写入的不止是表列的值,还有时间戳,例上图中,3为列值,TiKV中为3_100,其中,3为列值,100为start_ts的值
  • 只给事务第一行加一把主锁(pk)并写入Lock列簇中,其他的锁都指向这个锁。
    Commit阶段:
  • 在commit从PD获取到结束时间戳后,首先会在Write的CF里写入提交信息,即上图的put <3_110,100>,元素有3(事务ID),110(结束时间戳),100(开始时间戳)
  • 锁清理写入到Lock的CF里,即<3,(D,pk,3,100…)>,D表示Delete删除
    这里的三个列簇其实只有一把锁,整体完成才会写入

两阶段提交,多少还是有区别的

我明白了, 还是利用同一行不同cf可以原子更新的机制。

我再多问一个问题, 目前Rocksdb不能像Hbase那样支持存多版本数(CF 上同时存个版本的值), 上面你说的write列,是如何保存多版本的?所有的版本数据写在一起,读的时候时候全部捞出来吗?如果是这样如何快速根据读快照找捞到可读的版本?

rocksdb的底层实际上是一堆key-value键值对,TiDB中TiKV的数据存储的多版本数是通过MVCC实现的,通过在 Key 后面添加版本号来实现的,举例如下:
没有 MVCC 之前,
Key1 → Value
Key2 → Value

有了 MVCC 之后,TiKV 的 Key 排列是这样的:
Key1_Version3 → Value
Key1_Version2 → Value
Key1_Version1 → Value
……
Key2_Version3 → Value
Key2_Version2 → Value
Key2_Version1 → Value
……
对于同一个 Key 的多个版本,版本号较大的会被放在前面,版本号小的会被放在后面,这样当用户通过一个 Key + Version 来获取 Value 的时候,可以通过 Key 和 Version构造出MVCC的Key,也就是Key_Version。然后可以直接通过 RocksDB的SeekPrefix(Key_Version) API,定位到第一个大于等于这个 Key_Version 的位置。
当用户确信自己需要更长的读取时间时,比如在使用了 Mydumper 做全量备份的场景中(Mydumper 备份的是一致性的快照),可以通过调整 TiDB 中mysql.tidb表中的tikv_gc_life_time的值来调大 MVCC 版本保留时间,需要注意的是tikv_gc_life_time的配置是立刻影响全局的,调大它会为当前所有存在的快照增加生命时长,调小它会立即缩短所有快照的生命时长。过多的 MVCC 版本会拖慢 TiKV 的处理效率。

针对每个key存储多个版本的数据,怎么表示版本信息呢?
在TiKV中,我们只是简单地将key和timestamp结合成一个internal key来存储在RocksDB中。下面是每个CF的内容:
F_DEFAULT: (key,start_ts) → value
CF_LOCK: key → lock_info
CF_WRITE: (key,commit_ts) → write_info
将key和timestamp结合在一起地方法如下:
将user key编码为 memcomparable 的形式;
对timestamp按位取反,然后编码成big-endian的形式;
将编码后的timestamp添加到编码后的key之后。
例如,key key1和时间戳 3 将被编码成 “key1\x00\x00\x00\x00\xfb\xff\xff\xff\xff\xff\xff\xff\xfe”。这样同一个Key的不同版本在rocksdb中是相邻的,且版本比较大的数据在旧版本数据的前面。

可以的

此话题已在最后回复的 60 天后被自动关闭。不再允许新回复。