leveldb虽然支持多线程,但本质上并没有使用一些复杂的数据结构来达成无锁多写多读,而是坚持自然朴实的有锁单写多读。那么是不是只有对时间线产生变动的操作(Put, Compaction etc.)才需要上锁? 不是的。所有操作几乎都要在某一时间上锁来确保结果是线性的符合预期的。怎么讲? 用户在t1建立了快照,那就一定不能得到t2时才写入的数据。在t1建立快照这件事对数据库来说没有改变时间线(没有副作用, 不需要锁),但为了让快照成功建立,那就要上锁,不能有两个线程同时建立快照。所以多线程在很多情况下就是个伪命题,反正最后也会用各种锁模拟出顺序时间线,那还不如event loop呢。
Code:db/db_impl.cc(1117-1138)
Status DBImpl::Get(const ReadOptions& options,
const Slice& key,
std::string* value) {
Status s;
MutexLock l(&mutex_);
SequenceNumber snapshot;
if (options.snapshot != nullptr) {
snapshot =
static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
} else {
snapshot = versions_->LastSequence();
}
MemTable* mem = mem_;
MemTable* imm = imm_;
Version* current = versions_->current();
mem->Ref();
if (imm != nullptr) imm->Ref();
current->Ref();
bool have_stat_update = false;
Version::GetStats stats;
以上代码在锁的保护下完成了两件事,
- 生成一个SequenceNumber作为标记, 后续不管线程会不会被切出去, 结果都要相当于在这个时间点瞬间完成
- memtable, immemtable, Version, 由于采用了引用计数, 这里Ref()一下
快照建立完了, 接下来的操作只会有单纯的读, 可以把锁暂时释放
Code:db/db_impl.cc(1140-1154)
// Unlock while reading from files and memtables
{
mutex_.Unlock();
// First look in the memtable, then in the immutable memtable (if any).
LookupKey lkey(key, snapshot);
if (mem->Get(lkey, value, &s)) {
// Done
} else if (imm != nullptr && imm->Get(lkey, value, &s)) {
// Done
} else {
s = current->Get(options, lkey, value, &stats);
have_stat_update = true;
}
mutex_.Lock();
}
查询先找memtable, 再immemtable, 最后是SSTable, 这都很正常.
请注意我标注了黑科技那行的"LookupKey", 工程师用了些特别的技巧. 这个类主要的功能是把输入的key转换成用于查询的key. 比如key是"Sherry", 实际在数据库中的表达可能会是"6Sherry", 6是长度. 这样比对key是否相等时速度会更快.
Code:db/dbformat.cc(121-138)
LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
size_t usize = user_key.size();
size_t needed = usize + 13; // A conservative estimate
char* dst;
if (needed <= sizeof(space_)) {
dst = space_;
} else {
dst = new char[needed];
}
start_ = dst;
dst = EncodeVarint32(dst, usize + 8);
kstart_ = dst;
memcpy(dst, user_key.data(), usize);
dst += usize;
EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
dst += 8;
end_ = dst;
}
LookupKey格式 = 长度 + key + SequenceNumber + type
tricks:
- 在栈上分配一个200长度的数组, 如果运行时发现长度不够用再从堆上new一个, 可以极大避免内存分配
- 黑科技函数"EncodeVarint32", 一般key的长度不可能用满32bit. 大量很短的Key却要用32bit来描述长度无疑是很浪费的. 这个函数让小数值用更少的空间, 代价是最糟要多花一字节(8bit)
Code:db/dbformat.cc(47-73)
char* EncodeVarint32(char* dst, uint32_t v) {
// Operate on characters as unsigneds
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
static const int B = 128;
if (v < (1<<7)) {
*(ptr++) = v;
} else if (v < (1<<14)) {
*(ptr++) = v | B;
*(ptr++) = v>>7;
} else if (v < (1<<21)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = v>>14;
} else if (v < (1<<28)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = v>>21;
} else { // 最多用5字节
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = (v>>21) | B;
*(ptr++) = v>>28;
}
return reinterpret_cast<char*>(ptr);
}