事务在TiDB中的存储
分布式事务
提交的第一阶段,会用三个CF 来存放这些数据信息,
一类列簇对应一类键值对, 第一个CF(default)存放的是数据 的键值对。第二个存放的是锁信息。 第三个对应的是提交信息。
put<3_100,Frank> 3_100: primary_tso 表示是在事务时间戳100的时候对3这个主键进行修改。 注意增删改的操作,里面放的都是新数据。
< 3,(W,pk,3,100 …)> : 写锁,主键,事务相关信息. 注意一点,在TiDB当中,一个事务只有一个主锁,也就是分布式事务中的第一行加锁,这个锁就是主锁pk
put<3_110,100> : 会用第三个CF来存储提交信息,包含主行的标识,开始和提交的时间戳。
< 3,(D,PK,3,100 …)> < 3,(W,pk,3,100 …)> : 提交后,需要清理CF当中索的信息,它不是直接在里面删除这一行,而是新增一行, 告诉主键3上已经删除了锁(D)
读取的时候:它会先到write 的CF中查看 最近一次修改是什么时候,本例是110,并且通过110找到对应的事务开始是100,继而就能找到Frank
如果通过write中找不到,则表示这个记录正在被上锁,处于修改中,则这个记录不能直接读取。
- Write列: 当用户写入了一行数据时,如果该行数据长度小于255个字节,否则该行的数据会被存入到default列中。当然它更要用于存储 提价的信息
- default列: 用于存储超过255字节长度的数据。
< 1,(W,PK,1,100 …)>: 注意一点,在TiDB当中,一个事务只有一个主锁,也就是分布式事务中的第一行加锁,这个锁就是主锁pk
< 2,(W,@1,2,100 …)> :这是第二行,就不是主锁,@1 表示 我不是主,我的主锁在1那里。存的是锁的指向。
另外需要理解的是: 当插入修改值的时候,只需要在CF中插入新值即可。例如将Tom改成Jack,当别人去读取的时候,已经变成了Jack,就不用读Tom。
分布式事务如何解决原子性。
例如: 当前有个事务 里面的更新Tom为Jack是在node 1;更新Andy为Candy是在node 2 。 此时在第二阶段提交的时候 node 1成功,node 2失败。 因为节点1已经提交,相当于已经落盘持久化,但节点2此时由于故障 Lock并没有删除索引的信息,write中也没有提交信息。
此时,就可以通过主锁的机制来补齐相应的信息,流程如下: @1 可以找到对应的node 1,然后通过node 1 上的lock cf可以看到事务已经提交。此时在node 2 上,将write cf中补上<2_110,100> 在lock cf中添加清理索引的信息。这样就保障了分布式事务的原子性
MVCC
很多数据库都会实现多版本并发控制 (MVCC),TiKV 也不例外。设想这样的场景:
两个客户端同时去修改⼀个 Key 的 Value,如果没有数据的多版本控制,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。
这个版本号,可以使用TSO来控制生成(可以简单理解为时间戳)
TiKV 的 MVCC 实现是通过在 Key 后⾯添加版本号来实现,简单来说,没有 MVCC 之
前,可以把 TiKV 看做这样的:
Key1 -> Value
Key2 -> Value
……
KeyN -> Value
有了 MVCC 之后,TiKV 的 Key 排列是这样的:
Key1_Version3 -> Value
Key1_Version2 -> Value
Key1_Version1 -> Value
……
Key2_Version4 -> Value
Key2_Version3 -> Value
Key2_Version2 -> Value
Key2_Version1 -> Value
……
KeyN_Version2 -> Value
KeyN_Version1 -> Value
……
注意,对于同⼀个 Key 的多个版本,版本号较⼤的会被放在前⾯,版本号⼩的会被
放在后⾯(Key 是有序的排列),这样当⽤户通过⼀个 Key + Version 来获取 Value
的时候,可以通过 Key 和 Version 构造出 MVCC 的 Key,也就是 Key_Version。然后
可以直接通过 RocksDB 的 SeekPrefix(Key_Version) API,定位到第⼀个⼤于等于这个
Key_Version 的位置。
mvcc:就是快照读的意思,因为修改未提交的数据不能直接读取,此时需要通过快照。在TiDB当中不论增删改,它都是追加新值的方式(put)。所以TiKV很便捷的实现MVCC。
id = 1 如何读取,首先在write当中到对最近一次提交的TSO,110的时间戳,然后看到对应的是1 100 put < 1_100,Jack)> ,这样就把Jack读出来。读的就是以前值。
如果是写入, 则还需要看上面锁的信息,发现上面有个锁,并且没有释放索引的记录,则表示虽然id =1 在110的时候已经提交,但在115的时候又被写入了,则我当前不能够对这条记录进行写,它当前被这个 lock cf中的记录阻塞了。
id = 2的时候,读跟id = 1一样。
写可以看到lock当中没有,或者说已经释放。则我这个id =2 的记录可以写。
读跟以前原理一样
写: 这个时候可以看到 除了在Default中找到对应记录,还需要再Lock中查找,可以看到< 4,(W,@1,115 …)> 有记录,并且没有删除锁的记录,则继续找对应的主锁@1,再分析 < 1,(W,pk,115 …)> 发现它并没有提交,则表明当前这个分布式事务并没有提交,所以会被阻塞。