前言
在 LevelDB 中,写完 WAL 日志以后,就可以将数据写入到 MemTable 了。MemTable 是 LSM-Tree必不可缺的一个组件,主要作用如下:
- 写入的时候作为随机写转换为顺序写的buffer也是对数据进行排序的处理器
- 读取的时候作为热点数据(刚写入的数据)的cache,加快读取速度
- SSTable 的数据来源,初始的SSTable都是一个MemTable的持久化
在具体的使用中,MemTable 需要在内存中开辟堆空间,所以需要内存管理。客户端一般写入MemTable后就可以返回成功。
本文将针对MemTable 在 LevelDB 中的实现做一个简单的介绍,一起将客户端的写入过程也做了个介绍,会涉及到LevelDB如何控制并发等。
MemTable的实现在db/memtable.h
和db/memtable.cc
。内存管理的是实现是在util/arena.h
数据结构
MemTable 数据结构就是前面提到过的SkipList。LevelDB 是将key和value组合在一起成为一个SkipList中的Node,所以MemTable中还包含了一个比较器。这里对SkipList不再做具体的介绍,如果想深入了解,可以看下我前面写的SkipList原理和Java实现。
在db/memtable.h
中的具体实现为:
arduino
class MemTable {
public:
// MemTables are reference counted. The initial reference count
// is zero and the caller must call Ref() at least once.
explicit MemTable(const InternalKeyComparator& comparator);
MemTable(const MemTable&) = delete;
MemTable& operator=(const MemTable&) = delete;
// Increase reference count.
void Ref() { ++refs_; }
// Drop reference count. Delete if no more references exist.
void Unref() {
--refs_;
assert(refs_ >= 0);
if (refs_ <= 0) {
delete this;
}
}
// Returns an estimate of the number of bytes of data in use by this
// data structure. It is safe to call when MemTable is being modified.
size_t ApproximateMemoryUsage();
// Return an iterator that yields the contents of the memtable.
//
// The caller must ensure that the underlying MemTable remains live
// while the returned iterator is live. The keys returned by this
// iterator are internal keys encoded by AppendInternalKey in the
// db/format.{h,cc} module.
Iterator* NewIterator();
// Add an entry into memtable that maps key to value at the
// specified sequence number and with the specified type.
// Typically value will be empty if type==kTypeDeletion.
void Add(SequenceNumber seq, ValueType type, const Slice& key,
const Slice& value);
// If memtable contains a value for key, store it in *value and return true.
// If memtable contains a deletion for key, store a NotFound() error
// in *status and return true.
// Else, return false.
bool Get(const LookupKey& key, std::string* value, Status* s);
private:
friend class MemTableIterator;
friend class MemTableBackwardIterator;
struct KeyComparator {
const InternalKeyComparator comparator;
explicit KeyComparator(const InternalKeyComparator& c) : comparator(c) {}
int operator()(const char* a, const char* b) const;
};
typedef SkipList<const char*, KeyComparator> Table;
~MemTable(); // Private since only Unref() should be used to delete it
KeyComparator comparator_;
int refs_;
Arena arena_;
Table table_;
void PrintBuffer(char* buf, size_t i);
};
上面的实现中可以看到,MemTable 拒绝复制当前内部的数据,或者说拒绝使用拷贝构造函数和赋值来拷贝当前的数据。
arduinoMemTable(const MemTable&) = delete; MemTable& operator=(const MemTable&) = delete; 这两个方法的代码指的是禁止了MemTable的复制,也就是说一个MemTable不能通过拷贝构造函数构创建新对象的方式复制当前MemTable对象。也不能使用赋值运算符来创建一个新的对象并且指向当前的对象,赋值运算符也是会拷贝数据的。
能够创建一个MemTable的方式只有使用传入一个InternalKeyComparator
。也不能够复制当前MemTable中的数据。
MemTable中还有一个Ref,这个和gc中的引用是一个意思,如果这个refs中的数据没有到0,说明有人还在使用这个MemTable,那么就不能进行内存释放。
ApproximateMemoryUsage
当前已经使用的内存,这个命名是说这个是一个大概的值,我觉得挺神奇的,后面看下为什么是大概的值,是否可能会有并发访问结果不一样的情况。
NewIterator
创建一个迭代器
提供的读写接口就两个,一个是Get 一个是Add:
-
Add 返回值是void 入参分别为
- SequenceNumber seq 当前写入的sequence
- ValueType type // 类型,该类型只有两种,一个是删除,一个是写入
- const Slice& key // 写入的key
- const Slice& value)//写入的Value
-
Get 返回值是true 入参分别为:
- const LookupKey& key // 查询的Key值
- std::string* value 如果有数据就会写在value里面,也就是如果返回为true,那么值就在value里面
- Status* s 如果查询的值已经被删除,那么status会有一个error但是函数会返回true
私有域中主要包含了两个迭代器,Key的比较器,内存分配的Arena 和Table,这个Table就是SkipList。在析构函数里说的很清楚的就是只有在Unref里面可以调用,也就是当refs_的值小于等于0 的时候就可以释放当前的内存。
Add
add 是将当前的值写入到MemTable中,为了更好理解里面的并发控制和sequence,直接看db的Write操作,是在db/db_impl.cc
中的DBImpl::Write中,具体实现如下:
scss
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
Writer w(&mutex_); // 每次写入都会封装为一个Writer,这个在前面的Log中有了解,注意的是,Writer中传入了互斥锁
w.batch = updates; // 写入操作在Put的是已经封装为了WriteBatch
w.sync = options.sync; // 判断当前是否同步刷盘,也是Log中的操作
w.done = false; // 当前写入状态初始为false
// 锁a
MutexLock l(&mutex_); // 首先根据信号量初始化锁
writers_.push_back(&w); // 将写入操作放入一个writers队列中,注意因为有锁,所以同一时间只会有一个线程进入到writers中
while (!w.done && &w != writers_.front()) {
w.cv.Wait(); // 需要注意的是,在Wait阶段是会释放锁的
} // 使用队头的writer作为本次写入的writer
if (w.done) { // 如果本次的写入已经被完成,则直接返回写入的状态
return w.status;
}
// May temporarily unlock and wait.
Status status = MakeRoomForWrite(updates == nullptr); // 创建空间用于写入磁盘或者memTable
uint64_t last_sequence = versions_->LastSequence(); // 从当前的version中获取到最后使用的sequence
Writer* last_writer = &w; //本次写入的writer赋值给last_writer,注意为什么这里明明&w是队头却是lastwriter,是因为在后面的BuildBatchGroup方法中,会将本次批量写入的最后一个writer赋值给他
if (status.ok() && updates != nullptr) { // nullptr batch is for compactions
WriteBatch* write_batch = BuildBatchGroup(&last_writer); // 将当前队列中所有的writer里面的数据合并为一次写入
WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);// 设置本次批量写入的sequence,sequence每次写入都是递增的,保证了写入的顺序,也能够进行读取的MVVC
last_sequence += WriteBatchInternal::Count(write_batch); // 更新当前的lastSequence,write_batch 中包含了当前数据的大大小
// Add to log and apply to memtable. We can release the lock
// during this phase since &w is currently responsible for logging
// and protects against concurrent loggers and concurrent writes
// into mem_.
{
mutex_.Unlock(); // 释放队列锁,此时可以继续写入writers队列了。但是由于当前的writer 没有从队头移除,所以此时仍然等待在 w.cv.Wait();中
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch)); // 写入Log文件
bool sync_error = false;
if (status.ok() && options.sync) { // 写入成功,是否同步刷盘
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
if (status.ok()) {
status = WriteBatchInternal::InsertInto(write_batch, mem_); // 此处写入mem
}
mutex_.Lock(); // 再次获取到锁,暂停线程写入writers,注意的是,这个锁的释放是等到本次线程退出方法,调用MutexLock的析构函数达到释放锁的目的
if (sync_error) {
// The state of the log file is indeterminate: the log record we
// just added may or may not show up when the DB is re-opened.
// So we force the DB into a mode where all future writes fail.
RecordBackgroundError(status);
}
}
if (write_batch == tmp_batch_) tmp_batch_->Clear(); // 清理tmp_batch
versions_->SetLastSequence(last_sequence);// 设置sequence 到version中
}
while (true) {
Writer* ready = writers_.front();
writers_.pop_front();
if (ready != &w) {
ready->status = status;
ready->done = true;
ready->cv.Signal();
}
if (ready == last_writer) break;
} // 依次唤醒本次写入的后续writer,此时会从上面的while中继续调用,如果是头节点,而且已经被写入则直接返回,否则就继续执行上面的代码,该循环一直到本次写入的最后一个writer位置
// Notify new head of write queue
if (!writers_.empty()) { // 唤醒下一次调用
writers_.front()->cv.Signal();
}
return status;
}
算得上逐行解释了上面的代码。接下来看下如何使用一个互斥锁实现兼顾多线程顺序写入和效率的。
Add中的锁
先来看最开始的while循环中的cv.Wait()方法,在port/port_stdcxx.h
中,实现为:
c
void Wait() {
std::unique_lock<std::mutex> lock(mu_->mu_, std::adopt_lock);//ustd::unique_lock能实现自动加锁与解锁功能,第一个参数是指的传入的参数进行上锁,如果有第二个参数即 std::adopt_lock:表示这个互斥量已经被lock()过了,无需在本次构造函数中加锁,否则会报错。
cv_.wait(lock); // wait 方法会对互斥锁解锁,然后阻塞等待,一直到被notify唤醒。唤醒之后会再次获取锁,一直到获取锁成功后才继续往下执行。
lock.release(); // 检测当前锁是否没有释放,如果是则释放掉
}
也就是说每次写入在Wait过程中,会释放当前获取的锁,允许后面的writer 写入到writers中。
下面举例说明:
如果当前有t1
,t2
,t3
,并发写入。假设初始阶段writers为空:
t1
,t2
,t3
写入的时候,首先假设t1
首先执行MutexLock l(&mutex ); 即获取到了mutex 的锁,t1
被写入到writers中,而且是作为队头,所以不需要进入Wait状态,直接开始写,此时t2
,t3
在等待获取锁。t1
执行到了mutex_.Unlock();此时t1
的writer 还在队头,所以t2
,t3
被写入到writers中等待唤醒。- 此时writers中包含了
t2
,t3
,但是t1
只会写自己的数据,因为他是在合并后才释放锁让t2
,t3
进入的.t1
先写入Log,然后写入MemTable,此时不存在并发写的情况,因为其他的并发都会被放到writers中。 - 等到
t1
将自己的数据写入到Log 和MemTable后,t1
再次获取到锁,此时阻塞writers的中继续新增writer。 - 因为
t1
的没有进行操作合并,所以他不会唤醒其他的写,只会唤醒下一次的队头,但是此时并没有释放锁。只是唤醒了t2
,t2
在等待t1
释放锁。 t1
本次写入返回,方法栈中的MutexLock 被释放,然后t2
获取到锁,此时writers中的数据有t3
和t2
t2
的操作相比t1
多了一个合并数据和唤醒的动作。这里就不赘述了。
上文中一个就涉及到1个锁,却能够有效的将数据的顺序和并发全部完成。使用一个队列,将多线程写入转换为单线程写入,保证了顺序,也有效地保证了效率。
校验空间容量
在写入之前,还需要看下当前的内存空间,和level0的文件数,实现就在db/db_impl.cc
的MakeRoomForWrite方法中:
scss
Status DBImpl::MakeRoomForWrite(bool force) {
mutex_.AssertHeld(); // 确认当前的线程获取到了锁
assert(!writers_.empty()); // 有writer 操作
bool allow_delay = !force; // 是否运行缓冲,默认是1ms
Status s; // 返回的状态
while (true) {
if (!bg_error_.ok()) { // 这个bg_error是后台合并level0 的时候的一个操作
// Yield previous error
s = bg_error_;
break;
//1 如果允许等待(正常写入可以等待。force==updates==nullptr),并且当前的0层
// 文件触发了需要等待的条件(0 层文件大于等于8)
} else if (allow_delay && versions_->NumLevelFiles(0) >=
config::kL0_SlowdownWritesTrigger) {
// We are getting close to hitting a hard limit on the number of
// L0 files. Rather than delaying a single write by several
// seconds when we hit the hard limit, start delaying each
// individual write by 1ms to reduce latency variance. Also,
// this delay hands over some CPU to the compaction thread in
// case it is sharing the same core as the writer.
mutex_.Unlock();// 首先会释放锁,因为此时会等待操作进行完成,没必要不让后续的写入进入
env_->SleepForMicroseconds(1000);// 等到1ms
allow_delay = false; // Do not delay a single write more than once,每次写入最多运行等待一次
mutex_.Lock(); // 加锁,说明要开始干活了
//2 如果当前的内存足够,而且level0 的文件数量没有超过最大,说明有足够的内存和文件,直接返回stats
// write_buffer_size 大小为4MB,也就是说一个内存文件大小一般在大于4MB的时候就需要切换了
} else if (!force &&
(mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
// There is room in current memtable
break;
//3 如果正在执行内存文件的合并,则等待内存文件合并完成
} else if (imm_ != nullptr) {
// We have filled up the current memtable, but the previous
// one is still being compacted, so we wait.
Log(options_.info_log, "Current memtable full; waiting...\n");
background_work_finished_signal_.Wait();
//4 如果有太多的level0层文件(默认12)。则等待文件合并完成
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
// There are too many level-0 files.
Log(options_.info_log, "Too many L0 files; waiting...\n");
background_work_finished_signal_.Wait();
} else {
//5 如果当前的文件数量小于8,内存资源不够,而且没有进行合并,则说明需要创建一个新的内存文件
// Attempt to switch to a new memtable and trigger compaction of old
assert(versions_->PrevLogNumber() == 0);
// 文件的名称也就是num 也是version提供的
uint64_t new_log_number = versions_->NewFileNumber();
WritableFile* lfile = nullptr;
// 创建可写文件,创建失败则说明岗前的num可以重复使用
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
if (!s.ok()) {
// Avoid chewing through file number space in a tight loop.
versions_->ReuseFileNumber(new_log_number);
break;
}
// 释放当前的Log文件
delete log_;
// 关闭当前的Log文件
s = logfile_->Close();
if (!s.ok()) {
// We may have lost some data written to the previous log file.
// Switch to the new log file anyway, but record as a background
// error so we do not attempt any more writes.
//
// We could perhaps attempt to save the memtable corresponding
// to log file and suppress the error if that works, but that
// would add more complexity in a critical code path.
RecordBackgroundError(s);
}
// 释放内存
delete logfile_;
// 将上面创建的文件复制写Log,成为新的Log日志文件
logfile_ = lfile;
// 设置num
logfile_number_ = new_log_number;
// 将创建的文件赋值给Log中的writer
log_ = new log::Writer(lfile);
// 将当前mem_ 的指针复制给imm_,说明当前的mem已经准备刷到level0 了。
imm_ = mem_;
// 设置是has_imm_ 为true,这里的 memory_order_release 前面说过,就是不允许指令重排
has_imm_.store(true, std::memory_order_release);
// 创建新的MemTable,传入当前的比较器
mem_ = new MemTable(internal_comparator_);
// 给当前的mem 添加引用
mem_->Ref();
force = false; // Do not force another compaction if have room
//尝试调用后台合并
MaybeScheduleCompaction();
}
}
return s;
}
这里分配的空间核心还是内存是否超过4MB,以及当前level0 的数据是否超过配置的阈值。如果说调大上面的值肯定可以提高一定的吞吐量。但是后期合并的数据量也对应会增加,个人觉得如果key,value 都比较小,则4MB就足够了,但是如果每次都是超大的key和value,就可以考虑调大方法中的参数,避免频繁合并。
合并操作
合并操作个人觉得没有什么好了解的,里面有个小tips就是直接对writers的迭代iter++ 这样可以有效的避免其实只有一个还需要走下面的合并操作。
开始写入MemTable
在写入完Log 后就开始执行写入MemTable了。
ini
status = WriteBatchInternal::InsertInto(write_batch, mem_);
Status WriteBatchInternal::InsertInto(const WriteBatch* b, MemTable* memtable) {
MemTableInserter inserter;
inserter.sequence_ = WriteBatchInternal::Sequence(b);
inserter.mem_ = memtable;
return b->Iterate(&inserter);
}
首先是创建了一个inserter的对象,赋值mem和sequence,然后调用WriteBatch的迭代器写入。这么封装有什么好处呢?为什么不在MemTable里做一个循环直接往里面写呢?个人觉得是为了解耦,MemTable插入的数据就是简单的Slice 对象,而不用去考虑里面的batch,通过迭代器解析然后插入,能够将WriteBatch的职责和MemTable的职责做一个很好的区分。
在正式进入迭代方法之前,先来看下此时一个的WriteBatch 数据结构。这里就不贴全部的源码了,只是说下WriteBatch里面主要是一个Slice的key和Slice 的value。这些值最后都会被挡在一个string类型名为rep_参数中。
每次写入都会将当前操作类型即ValueType放入到req中,写入之后的seq为:
css
如果是kTypeValue则数据是 [seq预留8字节,全部为0][count][kTypeValue(1字节)][key_length][key_value][value_length][value_value]
如果是kTypeDeletion 则数据为[seq预留8字节,全部为0][count][kTypeDeletion(1字节)][key_length][key_value]
然后在BuildBatchGroup 中也只是对这个值进行一个追加,最后是一个大的WriteBatch,包含了n个写入,这个在WriteBatch 中是一个append,每次都会将需要append的数据截取12字节后面的数据,然后将count重新设置到被append的count中。执行完BuildBatchGroup 后,就会在前面的4个字节中写入sequence。
结合Log来看,这里的key,value的值都是使用的Varint32位,这也是为什么需要在Log中写入Fragement的原因了。
了解到了WriteBatch 的数据结构就不在去看WriteBatch::Iterate里面的源码里,其实就是根据数据结果解析为key和value,然后分为delete方法或者Put方法。但是需要注意的是,WriteBatch::Iterate的里面的Put方法最后走到了class MemTableInserter : public WriteBatch::Handler
这个类里面的Put方法里。这个方法会对核心就是对当前批次写入的sequence进行解析和插入到MemTable中,说明虽然我们合并了数据的写入,但是在写入Mem中的时候sequence 还是按照写入顺序+1 的。
arduino
void Put(const Slice& key, const Slice& value) override {
mem_->Add(sequence_, kTypeValue, key, value);
sequence_++;
}
void Delete(const Slice& key) override {
mem_->Add(sequence_, kTypeDeletion, key, Slice());
sequence_++;
}
LevelDB里面的迭代器目前已经了解了两个了,一个是MemTable中的Iterator,用来查询数据,还有一个就是当前的写入了,其实迭代器在LevelDB中使用比较多,后面全部过一遍再回头来看迭代器的使用。
Add 方法
看到这里,终于开始往MemTable 写数据了。具体实现比较简单,主要就是Key值的创建也就是SkipList中Node 的创建。具体代码实现在db/mem_table.cc
ini
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
const Slice& value) {
// Format of an entry is concatenation of:
// key_size : varint32 of internal_key.size()
// key bytes : char[internal_key.size()]
// tag : uint64((sequence << 8) | type) // 将type 放在seq后面
// value_size : varint32 of value.size()
// value bytes : char[value.size()]
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8;
const size_t encoded_len = VarintLength(internal_key_size) +
internal_key_size + VarintLength(val_size) +
val_size;
char* buf = arena_.Allocate(encoded_len); //创建当前encode 后的数据大小的内存,ie:指向0x8060f8
char* p = EncodeVarint32(buf, internal_key_size);// 将internal_key_size的值放入到buf中,返回指向 //0x8060f9
std::memcpy(p, key.data(), key_size);// 将key的值放入到p指针
p += key_size; //因为key的值有6个
EncodeFixed64(p, (s << 8) | type);
p += 8; //0x8060ff 仍然指向
p = EncodeVarint32(p, val_size);//0x806107
std::memcpy(p, value.data(), val_size);
assert(p + val_size == buf + encoded_len);
// for (size_t i = 0; i < encoded_len; ++i) {
// unsigned char c = static_cast<unsigned char>(buf[i]);
// std::bitset<8> binary(c); // 将字符转换为 8 位二进制
// std::cout << "Character: " << buf[i] << " Binary: " << binary << std::endl;
// }
table_.Insert(buf);
}
ValueType的值就是上面提到的MemTableInserter 中put或者delete来传入的。
然后就是写入MemTable的Key的的构建了,最后构建的结果如下:
css
[internal_key_size_length Varint64][key_length][key_value][tag[sequence<<8|type] 8字节][value_length][value_value]
然后就是开辟内存空间,传入的参数为上面的所有值长度之和:
scss
inline char* Arena::Allocate(size_t bytes) {
// The semantics of what to return are a bit messy if we allow
// 0-byte allocations, so we disallow them here (we don't need
// them for our internal use).
assert(bytes > 0);
// 如果当前的内存小于剩下的内存,则直接在剩下的内存中进行分配
if (bytes <= alloc_bytes_remaining_) {
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
return AllocateFallback(bytes);
}
如果上次开辟的空间未使用大于本次使用的空间,直接使用,否则就新创建空间:
scss
char* Arena::AllocateFallback(size_t bytes) {
// 当前的额分配的是否大于1k,如果大于1k直接开通当前的数量
if (bytes > kBlockSize / 4) {
// Object is more than a quarter of our block size. Allocate it separately
// to avoid wasting too much space in leftover bytes.
char* result = AllocateNewBlock(bytes);
return result;
}
// 如果超过1k,创建4k的内存
// We waste the remaining space in the current block.
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize;
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
在Log中提到,每次读取都是4kb,所以这里每一个Block也是4kb。仅仅分为两种情况,超过1kb的直接分配需要的内存数,否则直接分配4kb,具体的内存分配在AllocateNewBlock:
c
// 将创建的内存方在blocks_的链表中,然后给memory_usage_ 的值添加一下
//memory_usage_ 记录的就是当前memtable 使用的内存大小,如果超过配置的最大缓存值,会将内存数据写入到磁盘上
// 内存分配就是很简单的创建一个char数组,然后push到队列中。
char* Arena::AllocateNewBlock(size_t block_bytes) {
char* result = new char[block_bytes];
blocks_.push_back(result);
memory_usage_.fetch_add(block_bytes + sizeof(char*),
std::memory_order_relaxed);
return result;
}
可以看到内存分配非常简单,就是将分配一个char数组,然后放入到blocks的vector里面。为什么可以这么简单的管理呢?这个和Area的生命周期相关,因为一个MemTable包含了一个Area独占的对象,当前MemTable如果释放了空间,说明当前的MemTable没有被其他人引用,而且已经写入到level0的sstable里了,所以area里面的空间可以直接释放。因为不会有人正在访问该空间了。
分配完成以后,就直接写入到SkiptList里面,MemTable的传入的Comparator 也就是MemTable使用的Comparator了。
Get
首先还是看db/db_impl.cc
中的Get方法,实现如下:
scss
Status DBImpl::Get(const ReadOptions& options, const Slice& key,
std::string* value) {
Status s;
MutexLock l(&mutex_); // 加锁,这里加锁的主要原因是需要获取当前的全局变量versions_,然后获取到sequence的值
SequenceNumber snapshot;
if (options.snapshot != nullptr) {
snapshot =
static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
} else {
snapshot = versions_->LastSequence();
}
MemTable* mem = mem_; // 当前正在写的Memtable
MemTable* imm = imm_;// 准备收入level0的MemTable,只读MemTable
Version* current = versions_->current(); //当前的version对象
mem->Ref(); // 给mem 新增一个引用,避免读取过程中该mem被回收
if (imm != nullptr) imm->Ref();// 给imm 新增一个引用,
current->Ref(); // version也增加引用
bool have_stat_update = false;
Version::GetStats stats;
// Unlock while reading from files and memtables
{
mutex_.Unlock(); // 释放锁,因为当前影响读取的version 或者sequence已经获取到了,所以不在会访问这些全局变量,而且mem和mem_也不会被回收。这里留下个问题,如果说当前读取操作的时候,mem和imm 都满了,等着写,那么岂不是也要等待数据读完?个人觉得是不会的,因为Imm已经在此处变成了本地变量,所以如果合并的时候已经被清理,那么是否回收这个imm的对象就交给了当前查询过程中的这个方法,即变成了栈空间对象的回收
// First look in the memtable, then in the immutable memtable (if any).
LookupKey lkey(key, snapshot);// 创建查看的key,
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();
}
if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction();
}
mem->Unref();
if (imm != nullptr) imm->Unref();
current->Unref();
return s;
}
这个LookupKey 核心实现就是组装当前查的值
css
[key_length][user_key][tag]
总的来说还是通过internal_key 查找,所以查找的比较器就比较重要,这里来看下实现:
ini
int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
// Order by:
// increasing user key (according to user-supplied comparator)
// decreasing sequence number
// decreasing type (though sequence# should be enough to disambiguate)
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);
if (anum > bnum) {
r = -1;
} else if (anum < bnum) {
r = +1;
}
}
return r;
}
按照key的值和sequence的值进行比较,都是使用uint64_t比较的,因为sequence取的是后24位,所以大小比较key有效过滤key值相等不同seq的值。
总结
本文介绍了MemTable的实现,还包含了具体的写入过程中的锁。LevelDB对锁的处理,内存的处理是十分指的学习的。但是每次都将Value的值也一起放入到内存,写入到SkipList里,个人觉得对大对象的存储和查询造成了很多内存上的浪费和使用。是否可以考虑存储计算分离,将Value的值和Key值分开,然后不断修改Key中的Value的offset就行了。有个初步的设想,首先Key记录的是Value的offset,磁盘或者内存,在合并的时候去查询,查到数据执行更新,比自己旧的数据可以进行删除和回收。