内部的key结构
源码位置与说明
db/dbformat.h db/dbformat.cc: 定义了Internal Key、Lookup Key、InternalKeyComparator和InternalFilterPolicy
在levelDB中,存在着多种类型的key,而由于levelDB本身又是kv型的数据库,所以弄清楚这些key的结构与作用都是十分重要的。
其中,在levelDB中主要有以下三种类型的key,分别是User Key
、Internal Key
和Lookup Key
,我们后面就针对这三种类型的key进行阐述。
User Key
User Key顾名思义,其实就是用户插入的key-value中的key,是原始的key的形式,这种是最简单的情况,也就是读写键值对时提供的键,只是一个简单的字符串,一般用Slice来表示。
程序与levelDB交互式,使用的key就是Usr Key。
Internal Key
第二种类型的key是Internal Key,这是SSTable中实际存储的key,也就是这个持久化有序的Map的键。但同时Internal Key是一个复合概念,是有几个部分组合成的一个key,ParsedInternal Key就是对Internal Key分拆后的结果,先来看看ParsedInternal Key的定义,这是一个结构体struct:
1 |
|
这个结构体说明ParsedInternalKey由三部分组成:user_key、序列号以及类型组成。而实际上ParsedInternalKey本身就可以看成是Internal key。
然后我们看一个比较重要的函数。
1 |
|
这两个函数是将ParsedInternalKey编码成字符串,也就是把一个结构体编码成字符串,这样就成为了真正的Internal key。我们接来下看一下Internal key。
1 |
|
InternalKey类中的私有对象就是存放编码后的Internal key,通过上面那两个函数,我们已经知道ParsedInternalKey是怎么转换成Internal key的,所以此处就给出Internal key的编码格式:
1 |
|
可以看到Internal Key在User Key的后面增加了一个64位的整数,并且将这个整数分为两部分,低位的一个字节是一个ValueType,高位的7个字节是一个SequenceNumber。
ValueType是为了区分一个键是插入还是删除,删除其实也是一条数据的插入,但是是一条特殊的插入,通过在User Key后面附上kTypeDeletion来说明要删除这个键,而kTypeValue说明是插入这个键。
SequenceNumber是一个版本号,是全局的,每次有一个键写入时,都会加一,每一个Internal Key里面都包含了不同的SequenceNumber。SequenceNumber是单调递增的,SequenceNumber越大,表示这键越新,如果User Key相同,就会覆盖旧的键。所以就算User Key相同,对应的Internal Key也是不同的,Internal Key是全局唯一的。当我们更新一个User Key多次时,数据库里面可能保存了多个User Key,但是它们所在的Internal Key是不同的,并且SequenceNumber可以决定写入的顺序。
当用户写入时,将User Key封装成Internal Key,保留版本信息,存储到SSTable里,当需要读取时,将User Key从Internal Key里提取出来,所有User Key相同的Internal Key里面SequenceNumber最大的Internal Key就是当前的键,它对应的值就是当前值。
另外Internal Key的比较方式和User Key是不一样的,Options提供的是User Key的比较方式,而LevelDB内部会生成一个根据这个User Key的比较方式得到的Internal Key的比较方式。
当Internal Key里面包含的User Key不同时,直接用User Key的比较方式即可。否则,根据SequenceNumber比较,按SequenceNumber倒序排序。这样的好处就是,在全局有序的Map里,根据User Key排序,User Key相同的会排在一起,SequenceNumber大的Internal Key排在前面。当Seek一个User Key时,会定位到第一个符合条件的Internal Key,也就是具有最大的SequenceNumber的Internal Key。
除了比较器,布隆过滤器也会被改造,以适应User Key到Internal Key的转换。
我们再来分析以下Internal key向user key和ParsedInternalKey转换的部分。
1 |
|
这部分其实也是很简单的,就是对应关系而已。
Lookup Key
Lookup Key其实就是简单的在Internal Key前面加上键的长度,使用varint32编码,主要用在MemTable的查找上。
由于MemTable的底层是一个Skiplist,而LevelDB的Skiplist只存储了一个键,而没有值。LevelDB在存储Kv时,又是将键和值编码在一起存储的,使用的就是字符串的长度前缀编码。所以在MemTable里查找Key时,提供的Lookup Key就是编码值的一个前缀,刚好可以定位MemTable里相应的键。
1 |
|
我们可以看到LookupKey类中的接口可以返回memtable_key、internal_key和user_key。为了理解这一点,我们先来看一下LookupKey的构造。
其内部成员作为指针分别指向了memtable_key开头、internal_key开头和结束位置。通过这几个特殊位置的指针就可以解析出所有的三种类型key。
从memtable_key()可知MemtableKey就是LookupKey,即:memtableKey == lookupKey,而不是internalKey,是klength+internalKey。
有一个部分感觉需要重点说一下,就是虽然跳表说是不存储值,只存储键(key),但是MemTable的Add方法会把需要保存的key-value编码成长度前缀码,即相当于将key-value编码成一个'key'放入跳表中存储。
而对于查找操作,只会按照前缀编码键,构造上面图的前半部分(lookupkey),而SkipList::Iterator::Seek的实现,会将迭代器定位到大于等于查找键的第一个键的位置,读出这个键,然后比对里面的键和查找的键是否相同,相同的话,才会读取对应的值,否则就是键不存在。
几种key的联系与区别
首先,用户直接插入的key就是user key,这个key没有经过任何的变化和编码。
然后在levelDB中,会将user key的后面添加上序列号和类型,编程Internal key,这个key是用于SSTable的存储的。序列号是一个单调递增的数字,类型分为插入类型和删除类型。因为levelDB中不存在删除数据的说法,想要实现删除就需要插入一条类型为删除的数据。由于越大的序列号数据越新,所以读取时会返回序列号最大的数据。
在Internal key前面插入Internal key的长度,就形成了lookup key。这个lookup key用于MemTable,因为MemTable内部是跳表,只存储键(注意这句话该如何理解,按照上面的说法,其实是键和值都被编码成键了)。构造出lookupkey后就可以用于查找,迭代器会被定位到大于等于lookupkey的第一个键的位置,读出这个键,然后比对里面的键和查找的键是否相同,相同的话,才会读取对应的值,否则就是键不存在。
总结
以上就是LevelDB的三种键,是包含的关系:
- User Key是用户提供的键,是我们看到的键
- Internal Key是实际存储的键,支持版本号和Tag的功能
- Lookup Key则是为了查找MemTable而构造的键