记得大学刚毕业那年看了侯俊杰的《深入浅出MFC》,就对深入浅出这四个字特别偏好,并且成为了自己对技术的要求标准——对于技术的理解要足够的深刻以至于可以用很浅显的道理给别人讲明白。以下内容为个人见解,如有雷同,纯属巧合,如有错误,烦请指正。
因为leveldb很多类型的声明和实现分别在.h和.cc两个文件中,为了代码注释方便,我将二者合一(类似JAVA和GO类的定义方法),读者在源码中找不到我引用的部分属于正常现象。
目录
Slice
Slice作为leveldb最基础的类型之一,应用非常广泛,关键点在于leveldb的key和value两个类型都是Slice类型。其实Slice类型和std::string(std::string也是可以用来存储binary数据,protobuf就是用std::string实现的binary数据存储)非常类似,但是要比std::string轻量很多。不用多说,都在注释里:
// 代码源自leveldb/include/leveldb/slice.h
class Slice {
public:
// 没有任何参数的构造函数,默认数据指向一个空字符串,而不是NULL
Slice() : data_(""), size_(0) { }
// 通过字符串指针和长度构造Slice,此方法可以用于截取部分字符串,也可以用于binary类型数据
// 我好奇的是为什么没有用const void*作为d的参数类型,这样会更通用一点
Slice(const char* d, size_t n) : data_(d), size_(n) { }
// 通过std::string构造Slice,因为没有用explicit声明,说明可以Slice s = std::string()方式赋值
Slice(const std::string& s) : data_(s.data()), size_(s.size()) { }
// 通过字符串指针构造Slice,大小就是字符串长度
Slice(const char* s) : data_(s), size_(strlen(s)) { }
// 返回数据指针,返回类型是const char*而不是void*需要注意一下
const char* data() const { return data_; }
// 返回数据长度
size_t size() const { return size_; }
// 判断Slice是否为空,仅通过size_等于0,因为默认构造函数data="",不是NULL,所以不能用空指针判断
bool empty() const { return size_ == 0; }
// 重载了[]运算符,那么就可以像数组一样访问Slice了,返回的类型是char型
char operator[](size_t n) const {
assert(n < size());
return data_[n];
}
// 清空Slice
void clear() { data_ = ""; size_ = 0; }
// 删除前面一些数据
void remove_prefix(size_t n) {
assert(n <= size());
// 对于Slice来说就是指针偏移
data_ += n;
size_ -= n;
}
// 把Slice转换为std::string
std::string ToString() const { return std::string(data_, size_); }
// 比较函数,基本上和std::string是相同的比较方法
int compare(const Slice& b) const {
// 取二者最小的长度,避免越界
const size_t min_len = (size_ < b.size_) ? size_ : b.size_;
// 直接使用memcmp()函数实现
int r = memcmp(data_, b.data_, min_len);
// 二者相等还有其他情况,那就是连个Slice的长度不同时,谁的更长谁就更大
if (r == 0) {
if (size_ < b.size_) r = -1;
else if (size_ > b.size_) r = +1;
}
return r;
}
// 判断Slice是不是以某个Slice开头的,这个普遍是拿Slice作为key使用的情况,因为leveldb的key是字符串型的,而且经常以各种前缀做分类
bool starts_with(const Slice& x) const {
return ((size_ >= x.size_) && (memcmp(data_, x.data_, x.size_) == 0));
}
private:
// 就两个成员变量,一个指向数据,一个记录大小
const char* data_;
size_t size_;
};
// 同时还重载了连个全局的运算符==和!=,应该还是比较简单的哈
inline bool operator==(const Slice& x, const Slice& y) {
return ((x.size() == y.size()) && (memcmp(x.data(), y.data(), x.size()) == 0));
}
inline bool operator!=(const Slice& x, const Slice& y) {
return !(x == y);
}
Slice类型不像std::string有内存管理能力,所有的内存由Slice外部管理,这一点需要注意一下。
SequenceNumber
SequenceNumber是一个无符号64位整型的值,我们这里用“顺序号”这个名字。leveldb每添加/修改一次记录都会触发顺序号的+1。
// 代码源自leveldb/db/dbformat.h
typedef uint64_t SequenceNumber;
Varint
Varint是一种比较特殊的整数类型,它包含有Varint32和Varint64两种,它相比于int32和int64最大的特点是长度可变。我们都知道sizeof(int32)=4,sizeof(int64)=8,但是我们使用的整型数据真的都需要这么长的位数么?举个例子,我们的使用leveldb存储的键,很可能长度连一个字节都用不上,但是leveldb又不能确定用户键的大小范围,所以Varint就应运而生了。
因为Varint没法用具体的结构体或者标准类型表达,所以使用的时候需要编码/解码(亦或是序列化/反序列化)过程,我们通过代码就可以清晰的了解Varint的格式了。
// 代码源自leveldb/util/coding.cc
// 我们指的Varint32就是存储在dst中,按照Varint32格式封装的数据
char* EncodeVarint32(char* dst, uint32_t v) {
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
static const int B = 128;
// 小于128,存储空间为一个字节,[v]
if (v < (1<<7)) {
*(ptr++) = v;
}
// 大于等于128小于16K用两个字节存储[v(7-13位), v(0-6位)|128]
else if (v < (1<<14)) {
*(ptr++) = v | B;
*(ptr++) = v>>7;
}
// 大于等于16K小于2M用3个字节存储[v(14-20位),v(7-13位)|128, v(0-6位)|128]
else if (v < (1<<21)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = v>>14;
}
// 大于等于2M小于256M用4个字节存储[v(21-27位),v(14-20位)|128,v(7-13位)|128, v(0-6位)|128]
else if (v < (1<<28)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = v>>21;
}
// 大于等于256M用5个字节存储[v(28-32位),v(21-27位)|128,v(14-20位)|128,v(7-13位)|128, v(0-6位)|128]
else {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = (v>>21) | B;
*(ptr++) = v>>28;
}
return reinterpret_cast<char*>(ptr);
}
上面是int32编码成Varint32的源代码,其实他的编码风格有点类似utf-8,字节从低到高存储的是整型的从低到高指定位数,每个字节的最高位为标志位,为1代表后面(更高位)还有数,直到遇到一个字节最高位为0。所以每个字节的有效位数只有7位,这样做虽然看似浪费了一些空间,如果我们使用的整型数据主要集中在2M以内的话,那么我们反而节省了2个字节的空间。
解码以及Varint64相关的代码读者自行看吧,原理已经了解了,这些已经没有什么技术含量了。
ValueType
ValueType我们直译成“值类型”,在leveldb中,值的类型只有两种,一种是有效数据,一种是删除数据。因为值类型主要和对象键配合使用,这样就可以知道该对象是有值的还是被删除的。在leveldb中更新和删除都不会直接修改数据,而是新增一条记录,后期合并会删除老旧数据。
// 代码源自leveldb/db/dbformat.h
enum ValueType {
kTypeDeletion = 0x0, // 删除
kTypeValue = 0x1 // 数据
};
static const ValueType kValueTypeForSeek = kTypeValue; // 用于查找
在查找对象时,对象不能是被删除的,所以kValueTypeForSeek等于kTypeValue。
InternalKey
类型名字已经非常能代表意思了,虽然用户在使用leveldb的时候用Slice作为key,但是在leveldb内部是以InternalKey作为Key的。所以我们非常有必要了解一下这个类型到底和Slice有什么不同,而且为什么要这么实现。
// 代码源自leveldb/db/dbformat.h
class InternalKey {
private:
// 只有一个私有成员变量,也就是说把Slice变成了std::string类型
std::string rep_;
public:
// 构造函数,这个没参数,但是可以通过DecodeFrom()再设置
InternalKey() { }
// 内部键包含:用户指定的键Slice、顺序号、以及值类型
InternalKey(const Slice& user_key, SequenceNumber s, ValueType t) {
// 最终的内部键的格式为:[Slice]+[littleendian(SequenceNumber<<8 + ValueType)],代码实现读者自己看吧
AppendInternalKey(&rep_, ParsedInternalKey(user_key, s, t));
}
// 从一个Slice中解码内部键,就是直接按照字符串赋值,所以此处的Slice非user_key
// 而是别的InternalKey.Encode()接口输出的
void DecodeFrom(const Slice& s) { rep_.assign(s.data(), s.size()); }
// 把内部键编码成Slice格式,其实就是用Slice封装一下,注意调用这个函数后InternalKey对象不能析构
// 因为Slice不负责内存管理
Slice Encode() const {
assert(!rep_.empty());
// 直接返回std::string类型?上面我们介绍Slice时候说过,Slice有一个构造函数参数就是std::string类型
// 并且没有声明为explicit,所以可以将std::string赋值给Slice
return rep_;
}
// 返回用户键值,所以需要按照上面构造函数的格式提取出来,ExtractUserKey()读者自己看就行,很简单
Slice user_key() const { return ExtractUserKey(rep_); }
// ParsedInternalKeyd的类型下面会有介绍,比较简单
void SetFrom(const ParsedInternalKey& p) {
rep_.clear();
// 这个函数调用和构造函数里面调用的是一个函数,所以就不多说了
AppendInternalKey(&rep_, p);
}
// 清空接口
void Clear() { rep_.clear(); }
};
从代码我们可以得出一个结论,内部键格式是在用户指定的键(Slice)基础上追加了按照小端方式存储的(顺序号<<8+值类型),即便是用std::string类型存储的,但是已经不再是纯粹的字符串了。
LookupKey
当需要在leveldb查找对象的时候,查找顺序是从第0层到第n层遍历查找,找到为止(最新的修改或者删除的数据会优先被找到,所以不会出现一个键有多个值的情况)。由于不同层的键值不同,所以LookupKey提供了不同层所需的键值。
// 代码源自leveldb/db/dbformat.h
// 因为查找可能需要查找memtable和sst,所以存储的内容要包含多种存储结构的键值
class LookupKey {
public:
// 构造函数需要用户指定的键以及leveldb内部
LookupKey(const Slice& user_key, SequenceNumber sequence) {
// 获取用户指定的键的长度
size_t usize = user_key.size();
// 为什么扩展了13个字节,其中8字节(64位整型顺序号<<8+值类型)+5字节(内部键的长度Varint32)
size_t needed = usize + 13; // A conservative estimate
// 内部有一个固定大小的空间,200个字节,这样可以避免频繁的内存分配,因为200个字节可以满足绝大部分需求
char* dst;
if (needed <= sizeof(space_)) {
dst = space_;
} else {
dst = new char[needed];
}
// 记录一下起始地址,在对象析构的时候需要释放空间(如果是从堆上申请的空间)
start_ = dst;
// 起始先存储内部键的长度,这个长度是Varint32类型
dst = EncodeVarint32(dst, usize + 8);
// 接着就是用户指定的键值
kstart_ = dst;
memcpy(dst, user_key.data(), usize);
dst += usize;
// 最后是64位的(顺序号<<8|值类型),此处值类型是kValueTypeForSeek,和类名LookupKey照应上了
EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
dst += 8;
// 记录结束位置,可以用于计算各种类型键值长度
end_ = dst;
// 整个存储空间的结构为[内部键大小(Varint32)][用户指定键][顺序号<<8|值类型]
}
// 析构函数,因为有申请内存的可能性,所以析构函数还是要有的
~LookupKey() {
// 要判断一下内存是否为堆上申请的,如果是就释放内存
if (start_ != space_) delete[] start_;
}
// 获取memtable需要的键值,从这里我们知道memtable需要的是内存中全部内容
Slice memtable_key() const { return Slice(start_, end_ - start_); }
// 获取内部键
Slice internal_key() const { return Slice(kstart_, end_ - kstart_); }
// 获取用户指定键
Slice user_key() const { return Slice(kstart_, end_ - kstart_ - 8); }
private:
const char* start_; // 指向存储空间的起始位置
const char* kstart_; // 指向用户指定键/内部键的起始位置
const char* end_; // 指向键值的结尾
char space_[200]; // 这样可以避免频繁申请内存
};
Comparator
Comparator是个抽象类,主要用于数据键值比较的,毕竟leveldb是按照键有序存储的,所以比较器算是一个比较通用的类型。并且,leveledb里面定义的比较器有两个特殊函数接口挺有意思的,具体我们从代码定义上来看看:
// 代码源自leveldb/include/leveldb/comparator.h
class Comparator {
public:
// 虚析构函数,析构对象的时候会调用实现类的析构函数
virtual ~Comparator();
// 比较,这个和Slice的比较是一个意思,所以基本上就是用Slice.compare()实现的,直接用Slice.compare()不就完了么
// 为什么还要定义这个类呢,后面会有各种实现类读者就知道为什么了
virtual int Compare(const Slice& a, const Slice& b) const = 0;
// 获取比较器的名字,这个不是很重要,也就是每个比较器有一个名字,我看基本都是实现类的全名(含namespace)
virtual const char* Name() const = 0;
// 通过函数名有点看不出来什么意思,这个函数需要实现的功能是找到一个最短的字符串,要求在[*start,limit)区间
// 这是很有意思的功能,当需要找到以某些字符串为前缀的所有对象时,就会用到这个接口,等我们看到的时候会重点解释使用方式
// 如果比较简单的比较器实现可以实现为空函数
virtual void FindShortestSeparator(std::string* start, const Slice& limit) const = 0;
// 这个接口和上一个接口很像,只是输出最短的比*key大的字符串,没有区间限制,如果比较简单的比较器可以实现为空函数
virtual void FindShortSuccessor(std::string* key) const = 0;
};
因为FindShortestSeparator()和FindShortSuccessor()这两个接口看似与比较器没啥关系,但是这两个接口实现的功能严重依赖比较功能,所以leveldb做到了比较器里面。我搜遍了leveldb的所有源码,继承Comparator的类只有三个:leveldb_comparator_t,InternalKeyComparator和BytewiseComparatorImpl。其中最为重要的当属BytewiseComparatorImpl,我们需要先从这个类型入手,然后再介绍其他的类型,等看完后大家就明白为什么BytewiseComparatorImpl是最重要的了。
BytewiseComparatorImpl
因为Slice并没有规定Key具体类型,所以leveldb是支持用户自定义比较器的,在创建leveldb数据库对象的时候通过Option指定。大家需要注意一点,Option的构造函数默认是把比较器设定为BytewiseComparatorImpl的,也就是说BytewiseComparatorImpl是leveldb默认的比较器,所以我说它比较重要。直接上代码吧:
// 代码源自leveldb/util/options.cc
// 下面是Options的构造函数,我们可以看出comparator赋值是BytewiseComparator()的返回值
// BytewiseComparator()返回的就是BytewiseComparatorImpl对象指针
Options::Options()
: comparator(BytewiseComparator()),
...... {
}
// 代码源自leveldb/util/comparator.cc
// Bytewise顾名思义按照字节主意比较,所以原理比较简单,下面就是具体实现了
class BytewiseComparatorImpl : public Comparator {
public:
// 空的构造函数,也是,这种对象也不需要啥成员变量
BytewiseComparatorImpl() { }
// 实现获取名字的接口
virtual const char* Name() const {
return "leveldb.BytewiseComparator";
}
// 比较本身就依赖Slice.compare()函数就可以了,本身Slice.compare()就是用memcmp()实现的,符合按字节比较的初心
virtual int Compare(const Slice& a, const Slice& b) const {
return a.compare(b);
}
// 在Comparator定义的时候就感觉就比较模糊,这回看看是怎么实现的?
virtual void FindShortestSeparator(std::string* start, const Slice& limit) const {
// 二者取最小的长度,避免越界访问
size_t min_length = (std::min)(start->size(), limit.size());
// 名字比较明确,就是第一个字节值不同的索引值,初始为0,这一点应该比较好理解,如果和limit相同,+1肯定就比limit大了
// 所以要找到第一个和limit不同的字节,从理论上讲,应该*start<=limit,第一个不同的字符就是第一个比limit字节小的数
// 当然也不排除有传错参数的情况
size_t diff_index = 0;
// 找到第一个不相同的字节值的位置
while ((diff_index < min_length) && ((*start)[diff_index] == limit[diff_index])) {
diff_index++;
}
// 也就是说*start和limit是相同的或者说limit是以*start为前缀的,所以肯定找不到一个字符串比*start大还比limit小
if (diff_index >= min_length) {
} else {
// 取出这个字节值
uint8_t diff_byte = static_cast<uint8_t>((*start)[diff_index]);
// 这个字节不能是0xff,同时+1后也也要比limit相应字节小,至少能看出来0xff对于leveldb是有特殊意义的
if (diff_byte < static_cast<uint8_t>(0xff) && diff_byte + 1 < static_cast<uint8_t>(limit[diff_index])) {
// 把找到的那个字符+1,同时把字符串缩短到那个不同字符的位置,这样就是[*start,limit)区间最短的字符串了
(*start)[diff_index]++;
start->resize(diff_index + 1);
assert(Compare(*start, limit) < 0);
}
// 其他情况就不对*start做任何修改,认为*start本身就是这个字符串了,那我们举一个例子:
// *start=['a', 'a', 'a', 'c', 'd', 'e']
// limit=['a', 'a', 'a', 'b', 'd', 'e']
// 上面这种情况是我认为输出*start=['a', 'a', 'a', 'c', 'e']可能会更好,为什么此处不这么做呢?
// 除非就是用在列举以某个字符串为前缀的对象时候使用,找到第一个比前缀大的字符串
}
}
// 找到第一个比*key大的最短字符串
virtual void FindShortSuccessor(std::string* key) const {
// 获取键的长度
size_t n = key->size();
// 遍历键的每个字节
for (size_t i = 0; i < n; i++) {
// 找到第一个不是0xff的字节
const uint8_t byte = (*key)[i];
if (byte != static_cast<uint8_t>(0xff)) {
// 把这个位置的字节+1,然后截断就可以了,算是比较简单。
(*key)[i] = byte + 1;
key->resize(i+1);
return;
}
}
// 能到这里说明*key全部都是0xff,也就找不到相应的字符串了
}
};
BytewiseComparatorImpl实现比较简单,没有什么太复杂的逻辑,所以就不再多说写什么了。但是读者还是需要注意一下,上面很多地方提到了字符串,而且是用std::string作为类型,其实数据中可能存在'\0'。在很多情况需要用一些特殊的字符做分割,比如std::string a=['a', 'b', 'c', '\0', 'd'],a的长度是5而不是3。只是强行转换为const char*时表现为“abc”而已,这本身是char*的自身约束(0是字符串的结尾)。所以以后大家要习惯这种所谓的“字符串”,并且BytewiseComparatorImpl这种字节比较器名字还是比较贴切的。
InternalKeyComparator
顾名思义,主要用在内部的Key比较器,范围也确定了,功能也确定了。其实InternalKeyComparator的主要功能还是通过BytewiseComparatorImpl实现,只是在BytewiseComparatorImpl基础上做了一点扩展。
// 代码源自leveldb/db/dbformat.h
class InternalKeyComparator : public Comparator {
private:
// 这个是最关键的了,有一个用户定义的比较器,不用多说了,肯定是BytewiseComparatorImpl的对象啦
// 所以我前面说InternalKeyComparator绝大部分是通过BytewiseComparatorImpl实现的
const Comparator* user_comparator_;
public:
// 构造函数需要提供比较器对象
explicit InternalKeyComparator(const Comparator* c) : user_comparator_(c) { }
// 返回比较器的名字
virtual const char* Name() const {
return "leveldb.InternalKeyComparator";
}
// 比较两个Slice
virtual int Compare(const Slice& a, const Slice& b) const {
// 采用BytewiseComparatorImpl.compare()进行比较,但是比较的是刨除顺序ID的用户提供的键
// 为什么要刨除顺序ID呢?看看下面的注释就知道了
int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
// 用户提供的键相等,意味着遇到了对象删除或者修改操作,需要再比较一下序列号。
if (r == 0) {
// 要返序列化(解码)顺序号
const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
// 顺序ID越大,说明插入时间越晚,数据就越新,如果排序的话应该排在前面,这个在LSM算法有说明
// 这就是为什么要针对顺序ID要特殊处理一下
if (anum > bnum) {
r = -1;
} else if (anum < bnum) {
r = +1;
}
}
return r;
}
// 获取一个最短的字符串在[*start,limit)范围内
virtual void FindShortestSeparator(std::string* start, const Slice& limit) const {
// 先把顺序号去了,把用户传入的key提取出来
Slice user_start = ExtractUserKey(*start);
Slice user_limit = ExtractUserKey(limit);
// 调用比较器的FindShortestSeparator()会修改传入参数,所以先存在临时变量中
std::string tmp(user_start.data(), user_start.size());
// 调用BytewiseComparatorImpl.FindShortestSeparator()
user_comparator_->FindShortestSeparator(&tmp, user_limit);
// 看看是否找到了字符串,因为只要找到了那个字符串,长度肯定会被截断的。
if (tmp.size() < user_start.size() && user_comparator_->Compare(user_start, tmp) < 0) {
// 追加顺序ID,此时的顺序ID没什么大作用,所以用最大顺序ID就可以
PutFixed64(&tmp, PackSequenceAndType(kMaxSequenceNumber,kValueTypeForSeek));
assert(this->Compare(*start, tmp) < 0);
assert(this->Compare(tmp, limit) < 0);
start->swap(tmp);
}
}
// 获取一个比*key大的最短字符串,原理和FindShortestSeparator很相似,我就不做重复注释了
virtual void FindShortSuccessor(std::string* key) const {
Slice user_key = ExtractUserKey(*key);
std::string tmp(user_key.data(), user_key.size());
user_comparator_->FindShortSuccessor(&tmp);
if (tmp.size() < user_key.size() && user_comparator_->Compare(user_key, tmp) < 0) {
PutFixed64(&tmp, PackSequenceAndType(kMaxSequenceNumber,kValueTypeForSeek));
assert(this->Compare(*key, tmp) < 0);
key->swap(tmp);
}
}
// 获取用户传入的比较器指针
const Comparator* user_comparator() const { return user_comparator_; }
// 比较内部键,内部键存储格式就是[用户键][顺序号<<8|值类型],所以重新封装成Slice后复用上面提到的比较函数
int Compare(const InternalKey& a, const InternalKey& b) const {
return Compare(a.Encode(), b.Encode());
}
};
看上面的代码就明白为什么叫做InternalKeyComparator了,因为leveldb内部存储键的时候在用户提供的键之上追加了顺序号,所以比较的时候略有不同。
leveldb_comparator_t
这个是转为C接口设计的比较器,非常简单,读者自己看代码了解一下。
Iterator
用过标准库(stl)容器的对迭代器不会陌生,用于遍历(可以条件遍历)容器中的元素使用。在leveldb中也有迭代器,毕竟leveldb可以想象成一个容量更大、功能更强的std::map嘛。在leveldb中,对Iterator进行了抽象,不同的存储类型自行实现具体的迭代器,我们这里只对Iterator的每个接口的功能做说明,具体实现会在其他文章中说明。
// 代码源自leveldb/include/leveldb/iterator.h
class Iterator {
// 为了方便理解构造函数和析构函数,代码上把私有成员变量和类型定义放到前面
private:
// 定义清理类型
struct Cleanup {
CleanupFunction function; // 清理函数,类型定义下面有定义
void* arg1; // 清理函数的参数1
void* arg2; // 清理函数的参数2
Cleanup* next; // 单向链表,指向下一个清理对象
};
// 所有需要清理的内容
Cleanup cleanup_;
public:
// 构造函数,初始是没有任何需要清理的对象的
Iterator() {
cleanup_.function = NULL;
cleanup_.next = NULL;
}
// 析构函数
virtual ~Iterator() {
// 看看是否有任何需要清理的对象
if (cleanup_.function != NULL) {
// 清理掉对象
(*cleanup_.function)(cleanup_.arg1, cleanup_.arg2);
// 遍历链表上的其他清理对象,逐一清理
for (Cleanup* c = cleanup_.next; c != NULL; ) {
(*c->function)(c->arg1, c->arg2);
Cleanup* next = c->next;
delete c;
c = next;
}
}
}
// 获取迭代器当前是否正常,比如到了结束为止该函数就会返回false
virtual bool Valid() const = 0;
// 定位到第一个对象为止
virtual void SeekToFirst() = 0;
// 定位到最后一个对象位置
virtual void SeekToLast() = 0;
// 定位到Slice指定的对象位置,如果没有对象,那么Valid()返回false.
virtual void Seek(const Slice& target) = 0;
// 定位到下一个对象,等同于stl容器迭代器的++
virtual void Next() = 0;
// 定位到前一个对象,等同于stl容器迭代器的--
virtual void Prev() = 0;
// 获取迭代器当前定位对象的键,前提是Valid()返回true
virtual Slice key() const = 0;
// 获取迭代器当前定位对象的值,前提是Valid()返回true
virtual Slice value() const = 0;
// 返回当前的状态
virtual Status status() const = 0;
// 定义清理函数类型
typedef void (*CleanupFunction)(void* arg1, void* arg2);
// 注册清理对象
void RegisterCleanup(CleanupFunction function, void* arg1, void* arg2) {
assert(func != NULL);
Cleanup* c;
// 如果当前清理对象链表是空的,那就把当前的清理对象记录在表头
if (cleanup_.function == NULL) {
c = &cleanup_;
} else {
// 创建新的清理对象放在表头的下一个位置,因为没有规定清理顺序,所以这种做法效率最高
c = new Cleanup;
c->next = cleanup_.next;
cleanup_.next = c;
}
// 记录清理对象的清理函数和参数
c->function = func;
c->arg1 = arg1;
c->arg2 = arg2;
}
};
迭代器的定义还是比较简单的,相比于stl增加了清理对象的内容,我会在其他章节说明都清理什么内容,这主要看具体的存储实现才行。
AtomicPointer
原子指针是通过原子操作访问的指针(不是访问指针指向的内存值,是指针本身的值),相比于普通指针(比如void*),对指针的获取和设置都支持原子操作的接口,同时也支持普通的访问方式。我们来看看AtomicPointer的定义:
// 代码源自leveldb/port/atomic_pointer.h
// leveldb/port目录是根据系统、CPU的不同自行实现的适配目录,所以我们只讲接口,不讲实现
class AtomicPointer {
private:
// 指针
void* rep_;
public:
// 默认构造函数
AtomicPointer() { }
// 提供指针的构造函数
explicit AtomicPointer(void* p) : rep_(p) {}
// 正常方式的获取/设置指针,比较简单,不用多说了
inline void* NoBarrier_Load() const { return rep_; }
inline void NoBarrier_Store(void* v) { rep_ = v; }
// 原子获取指针是通过内存屏障实现的,leveldb的内存屏障是阻止编译器乱序,但不能阻止CPU乱序执行(在SMP体系下)
// 此处使用内存屏障的最主要原因是因为该函数是inline,在不知道使用函数的上下代码的情况下,可能会被编译器优化掉
// 我们这里举一个比较简单的例子:void* ptr = NULL;
// ......(等待其他线程设置指针)
// ptr = Acquire_Load();
// 此时从编译器的角度来看ptr和后面的代码没有任何依赖,那么编译器可以先赋值ptr在等待其他线程设置
// 这个本身就完全改变了我们想要的结果
inline void* Acquire_Load() const {
void* result = rep_;
MemoryBarrier();
return result;
}
// 原子设置指针是通过内存屏障实现的,同样的道理,因为是inline函数,此处的v可能会在调用本函数前被其他线程修改
// 此处的内存屏障就可以保证赋值语句不会被编译器优化而被提前执行
inline void Release_Store(void* v) {
MemoryBarrier();
rep_ = v;
}
};
从代码上看,是否采用内存屏障访问指针,感觉都没什么区别,因为二者都是inline,不存在调用函数带来的开销,二者的区别在于使用时的上下文。所以,我在其他介绍leveldb的文章中但凡用到源自指针的地方我都会说明为什么使用带有内存屏障的方式访问指针。