近来在阅读TiDB
事务方面的代码,所以写一些学习记录。
点查询指的是获取单条数据的SQL
语句,相比于范围查询,点查询的执行过程要简单很多,原因有以下几点:
- 点查询的结果只可能在一个 region 内
- 点查询只会涉及到一个 key 上的锁处理
将TiDB
测试用例中的点查询稍作修改:
1 | use test; |
在完成建表和插入数据后,我们进行了两个查询,其中第一条SQL
可以通过在主键id
的等于条件查询条件知道最多有一条结果;第二条SQL
可以通过c
和d
的等于查询条件结合unique c_d (c, d)
的约束知道最多有一条结果。然而在实际执行时,还需要考虑许多情况。
执行过程
构造 key
我们先在不考虑优化的情况下谈论执行过程(即严格按照SI
的含义,对点查询取一个单独的快照来执行),TiKV
是一个分布式KV
数据库,即想要在TiKV
中查出值来,首先需要有一个key
,所以要先在TiDB
里面把key
给构造出来。
这里以point(id int primary key, c int, d varchar(10), unique c_d (c, d))
表为例,有两种构造出点查询的方式。
1 | select * from point where id = 1; |
为了分析便利,这里列出了接下来可能会用到的ASCII
表的内容。
16进制 | 字符 |
---|---|
0x5f | _ |
0x62 | b |
0x69 | i |
0x72 | r |
0x74 | t |
对于第一条查询,因为point
表中的主键字段为id
,所以能够直接通过id
来构造key
;而对于第二条查询,我们只知道c
和d
的约束条件,需要先通过已知的条件确定主键的值再进行查询。
我有意的将key
拆成了几段来进行分析
第一段
74 800000000000002d 5f72
和74 800000000000002d 5f69
第二段
8000000000000001
和8000000000000001 038000000000000002 016200000000000000 f8
第一段
74 800000000000002d 5f72
和74 800000000000002d 5f69
是两种查询类型,其中还包括了表信息。
其中800000000000002d
代表了表id
,用int64
来表示,但是在编码的时候把符号位强行置为了1。
查ASCII
表之后,我们可以将这两个key
写成t45_r
和t45_i
,可以参考以下代码。
1 | // github.com/pingcap/tidb/tablecodec/tablecodec.go |
那么很容易理解,recordPrefixSep
是要查询记录,而indexPrefixSep
是要查询索引,回到上面,select * from point where c = 2 and d = 'b'
这条SQL
要经过两次查询,第一次通过unique
索引查询到主键索引,第二次再通过主键索引查询记录值。
第二段
根据刚才的分析,8000000000000001
很容易理解,指的就是索引(这里是id
列)的值。
8000000000000001 038000000000000002 016200000000000000 f8
是一条对索引进行查询的key
的值。
8000000000000001
代表的是索引的id
(即使在一张表内也可能存在多个索引)038000000000000002
中03
代表是整数类型,8000000000000002
代表查询的值为2016200000000000000f8
中01
代表是bytes
类型,6200000000000000f8
是字符串b
编码后的结果,0x62
是b
,后面一串0
是为了对齐,最后的0xf8
表示 padding 长度,此处为 7,0xff - 7 = 0xf8
,详细的编码方式可以参考 TiDB代码
查询 key
在有了key
之后,TiDB
会向对应region
所在的TiKV leader
发出RPC
请求,此时,我们的问题变成了如何从一个带有分布式事务的KV
存储引擎上查询一个key
。
KV
存储引擎需要考虑的问题:
- 1 如果尝试读取一个正在
commit
中的key
,并且commit ts
小于get ts
,那么应该读到commit
完成之后的值 - 2 处理 Internal Read 的情况
TiKV
会首先拿取一个snapshot
,然后检查这个key
上存在的锁,锁信息存在一个叫lock
的CF
中,如果存在锁,则说明这个值可能有变化,即上面所说的问题1,此时TiKV
会将标志位met_newer_ts_data
从NotMetYet
改为Met
,然后执行check_ts_conflict
函数。
1 | /// Checks whether the lock conflicts with the given `ts`. If `ts == TimeStamp::max()`, the primary lock will be ignored. |
通过锁检查后,met_newer_ts_data
这个标志位可能会有两种状态,NotMetYet
和Met
。
对于NotMetYet
的情况,没有遇到这个key
上存在的锁,所以只需要拿取最新数据就可以了。取数据的过程涉及到MVCC
的操作,这里不会对其详细介绍(其实我也不大懂),可以参考TiKV 源码解析系列文章(十三)MVCC 数据读取。
1 | let mut use_near_seek = false; |
emm 这 blog 写着写着发现挖了好多坑,搞懂之后一定填…