从零实现LevelDB 3. MemTable实现

在本节,我们将会了解并实现:

  1. LevelDB的内部键InternalKey和LookupKey及对应的比较器
  2. Memtable

本节的代码包括InternalKey实现InternalKey和UserKey比较器实现memtable实现

InternalKey

介绍

当用户调用db->Put(a, b)时,LevelDB会对用户传入的Key(下称UserKey)进行内部封装处理,封装后的Key称为InternalKey。

InternalKey由UserKey、SequenceNumber(序列号)和ValueType(类型)组成,其组成如下图。

可以看到,InternalKey在UserKey的基础上,增加了两部分:7byte的序列号和1byte的ValueType。

其中,ValueType是为了区分键的操作类型,即插入或删除。

在第一节介绍过,LevelDB中的删除也是"插入"。当删除时,会将ValueType置为kTypeDeletion,代表此时的键值是要删除,而如果ValueType是kTypeValue,则说明键值是新插入。

Golang 复制代码
const (
	TypeDeletion InternalKeyKind = iota
	TypeValue
	TypeError
	TypeValueForSeek = TypeValue
)

在这里,我们暂时忽略TypeError和TypeValueForSeek类型。

LevelDB有一个全局唯一的序列号,每次写入时该序列号都会递增,而SequenceNumber就是这个序列号,每个InternalKey都包含不同的SequenceNumber,且该序列号单调递增。

所以在LevelDB中,每一个InternalKey都是不一样的,因为哪怕UserKey相同,其SequenceNumber也是不同的。

SequenceNumber越大,表示对应的键越新。在UserKey相同的情况下,新的键会覆盖旧的键。

InternalKey实现

Golang 复制代码
func MakeInternalKey(ukey []byte, ktype InternalKeyKind, seqNum uint64) InternalKey {
	var buf = bytes.NewBuffer([]byte{})
	binary.Write(buf, binary.LittleEndian, ukey)
	binary.Write(buf, binary.LittleEndian, (seqNum<<8)|uint64(ktype))
	return buf.Bytes()
}

func (ikey InternalKey) UserKey() []byte {
	if len(ikey) < 8 {
		return []byte{}
	}
	return ikey[:len(ikey)-8]
}

func (ikey InternalKey) Kind() InternalKeyKind {
	if len(ikey) < 8 {
		return TypeError
	}
	return InternalKeyKind(ikey[len(ikey)-8])
}

func (ikey InternalKey) SeqNumber() uint64 {
	if len(ikey) < 8 {
		return 0
	}
	i := len(ikey) - 7
	seqNum := uint64(ikey[i+0])
	seqNum |= uint64(ikey[i+1]) << 8
	seqNum |= uint64(ikey[i+2]) << 16
	seqNum |= uint64(ikey[i+3]) << 24
	seqNum |= uint64(ikey[i+4]) << 32
	seqNum |= uint64(ikey[i+5]) << 40
	seqNum |= uint64(ikey[i+6]) << 48
	return seqNum
}

InternalKey比较器

在我们了解了InternalKey的格式与组成后,那么在本节,我们将了解如何比较两个InternalKey的大小,其规则比较简单:

如果UserKey不同,那么直接比较UserKey即可;

如果UserKey相同,那么我们则要比较SequenceNumber,序列号越大,说明是新插入的数据,则比另外的InternalKey更"小",要排在更前面(排在前面的数据优先级大于后面的数据)。

对于InternalKey的比较器实现如下。

Golang 复制代码
func (InternalKeyComparer) Compare(a, b []byte) int {
	aKey := ikey.InternalKey(a)
	bKey := ikey.InternalKey(b)
	r := UserKeyComparator.Compare(aKey.UserKey(), bKey.UserKey())
	if r == 0 {
		anum := aKey.SeqNumber()
		bnum := bKey.SeqNumber()
		if anum > bnum {
			r = -1
		} else if anum < bnum {
			r = 1
		}
	}
	return r
}

在Compare中,首先先将传入的参数转化成InternalKey类型,然后先调用UserKey的比较器进行比较,如果相等,再通过序列号进行比较。

LookupKey

LookupKey是为了查找而生的,它在InternalKey的基础上增加了InternalKey的长度。

Golang 复制代码
type LookupKey []byte

// InternalKey_Length(uint32) | InternalKey
func MakeLookupKey(ukey []byte, num uint64) LookupKey {
	ikey := MakeInternalKey(ukey, TypeValueForSeek, num)
	ikey_len := uint32(len(ikey))
	var buf = bytes.NewBuffer([]byte{})
	binary.Write(buf, binary.LittleEndian, ikey_len)
	binary.Write(buf, binary.LittleEndian, ikey)
	return LookupKey(buf.Bytes())
}

func (lkey LookupKey) MemtableKey() []byte {
	return lkey
}

func (lkey LookupKey) InternalKey() InternalKey {
	return InternalKey(lkey[4:])
}

func (lkey LookupKey) UserKey() []byte {
	return lkey[4 : len(lkey)-8]
}

从MakeLookupKey的实现我们知道,它首先构造一个用于寻找的InternalKey(TypeValueForSeek),然后将这个InternalKey的长度和InternalKey写人缓存后返回。

Memtable实现

Memtable是基于跳表实现的,其定义如下。

Golang 复制代码
type Memtable struct {
	table   *skiplist.Skiplist
	memSize int
}

其中,memSize代表Memtable的大小。

此外,如果想为了后续的扩展,可以将table的类型改为有序数据结构接口,而不局限在*skiplist.Skiplist中,这样,后续如果想更换其他有序内存数据结构,也不用改动原有代码。

基本操作

插入

前文已经提到,当用户传入UserKey时,会将其封装成InternalKey,所以当用户要插入(UserKey, UserValue)时,在内部就变成了(InternalKey, UserValue)了。

在字节序列中,为了能获取InternalKey和UserValue的边界,我们还要在其前面加上各自的长度。

所以,最终的存储格式是InternalKey的长度 + InternalKey + UserValue的长度 + UserValue,如makeMemtableEntry函数所示。

Golang 复制代码
func makeMemtableEntry(seq uint64, ktype ikey.InternalKeyKind, ukey, uvalue []byte) []byte {
	var buf = bytes.NewBuffer([]byte{})
	ikey := ikey.MakeInternalKey(ukey, ktype, seq)
	binary.Write(buf, binary.LittleEndian, uint32(len(ikey)))
	binary.Write(buf, binary.LittleEndian, ikey)
	binary.Write(buf, binary.LittleEndian, uint32(len(uvalue)))
	binary.Write(buf, binary.LittleEndian, uvalue)
	return buf.Bytes()
}

有了makeMemtableEntry这个函数,memTable的Add实现就非常简单,只要直接Put并更新table的大小即可。

Golang 复制代码
func (memTable *Memtable) Add(seq uint64, ktype ikey.InternalKeyKind, ukey, uvalue []byte) {
	memEntry := util.MakeMemtableEntry(seq, ktype, ukey, uvalue)
	memTable.memSize += len(memEntry)
	memTable.table.Put(memEntry)
}

再次强调一次,删除也是属于插入,也是调用Add接口,因为删除只是将ktype的类型改为删除类型。

查找

在查找时,我们传入的不是ukey,而是lookupkey。

在上文有提到,lookupkey是由序列号和userkey组成而来。序列号由快照传入,当未传入快照时,则设置为LevelDB中最大的序号。

在跳表中,相同的UserKey,序号越大反而越小(越新),

因此通过Seek后,迭代器会找到第一个大于等于lookupkey的值,如果该值的userkey与lookupkey的userkey相同,

就是此时DB中的最新记录,否则,DB中不存在该userkey。

Golang 复制代码
func (memTable *Memtable) Get(lkey ikey.LookupKey) ([]byte, error) {
	ukey := lkey.UserKey()
	it := memTable.table.NewIterator()
	// 找到第一个>=lkey的值
	it.Seek(lkey)
	if it.Valid() {
		memEntry := it.Key()
		key_length := binary.LittleEndian.Uint32(memEntry)
		internal_key := ikey.InternalKey(memEntry[4 : 4+key_length])
		if comparator.UserKeyComparator.Compare(ukey, internal_key.UserKey()) == 0 {
			if internal_key.Kind() == ikey.TypeValue {
				value_length := binary.LittleEndian.Uint32(memEntry[4+key_length:])
				value := memEntry[8+key_length : value_length+8+key_length]
				return value, nil
			} else if internal_key.Kind() == ikey.TypeDeletion {
				return nil, errors.ErrDeletion
			} else {
				return nil, errors.ErrUndefined
			}
		}
	}
	return nil, errors.ErrNotFound
}

支持迭代器

总体来说,memTable的迭代器都是基于跳表的迭代器实现,其中比较有变化的就是Key的实现。

因此用户传入的UserKey已经被封装成InternalKey,所以,Iterator中,Key返回的是InternalKey。

Golang 复制代码
// 返回InternalKey
func (it *Iterator) Key() []byte {
	memEntry := it.listIter.Key()
	key_length := util.GetUint32(memEntry)
	internal_key := ikey.InternalKey(memEntry[4 : 4+key_length])
	return []byte(internal_key)
}
相关推荐
drebander2 分钟前
SQL 实战:分页查询的多种方式对比与优化
数据库·sql
Run Out Of Brain1 小时前
MySQL与标准SQL的区别
数据库·sql·mysql
YHPsophie1 小时前
XQR5VFX130-1CN1752V,,具有高度的可编程性和灵活性的FPGA中文技术资料
数据库·fpga开发·信息与通信·fpga
李歘歘1 小时前
MySQL数据库——常见慢查询优化方式
数据库·sql·mysql·索引·数据库优化·慢查询
落魚京2 小时前
maven大面积依赖报错
java·数据库·maven
梁萌2 小时前
在docker中对MySQL快速部署与初始数据
运维·数据库·mysql·docker·容器
言之。2 小时前
【MySQL】 SQL优化讲解
数据库·sql·mysql
m0_748248232 小时前
MySQL——数据类型
android·数据库·mysql
开心工作室_kaic2 小时前
springboot502基于WEB的牙科诊所管理系统(论文+源码)_kaic
开发语言·前端·数据库·智能手机·美食
gzpingesoft3 小时前
Midjourney Imagine API 使用
数据库·midjourney