上一篇介绍了leveldb初始化流程,本篇开始介绍存储流程。对于leveldb来说,删除数据实际是插入数据,只是将类型设置为删除即可,并不会真正将数据从磁盘中删除.那么什么时候真正删除呢?在压缩过程中,具体在压缩流程中介绍。
一、插入
1.1、总体流程图
调用接口Put即可完成插入数据。那么内部实现是什么呢?首先来看一下总体流程图:
说明:
1) 调用接口Put插入数据,首先会先保存到.log文件文件中,这样做的好处是当异常重启保证数据不丢失 。
2) 插入到MemTable中,我们在之前篇章有介绍到MemTable底层实现是跳表,leveldb控制了MemTable使用内存空间大小,当插入数据使MemTable占用内存达到了门限值则会将MemTable转换成Immutable MemTable,反之亦然。
3)leveldb会创建一个新线程会将Immutable MemTable中的数据写到入level0文件中,写入完成后释放内存,紧接着判断是否需要进行压缩处理。
1.2、批任务
以下是Put整体流程图,相对比较简单.但是leveldb在内部却做了很复杂操作,leveldb在调用Put后会将key-value封装成WriteBatch(批任务),然后调用Write方法,将WriteBatch投递到生产者队列中,最后由消费者进行处理。
1.2.1、批任务格式
一个WriteBatch是有内部存储格式,具体存储格式如下说是
/**
* 成员rep_ 内存表现形式
*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* | Sequnce number |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* ~ type-key-value (不定长) ~
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | |
* ~ type-key-value (不定长) ~
* | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
字段名 | 类型大小 | 说明 |
Sequnce number | 8字节 | 序列号,每个WriteBatch都有唯一的序号 |
count | 4字节 | 代表type-key-value有多少个,一个WriteBatch可以同时操作多个key-value.固定大小,按照小端序存储 |
type-key-value | 可以长度 | 1)type(1字节)取值: kTypeDeletion(删除)和kTypeValue(插入) 2)key的存储: 先存储key-length再存储key实际内容,其中key-length进行7bit数字压缩出存储 3)valu的存储方式和key一样,当类型为kTypeDeletion时value部分为空 |
具体参考代码为WriteBatch::Write,源码比较简单这里不在贴出。
1.2.2、代码实现
/**
* 写操作
* @param options 写操作选项
* @param my_batch 批任务
*/
Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) {
Writer w(&mutex_); // 结构Writer在本文件 创建条件变量
w.batch = my_batch;
w.sync = options.sync;
w.done = false;
MutexLock l(&mutex_); // 创建互斥锁
writers_.push_back(&w);
//生产者消费者模式
//多线程并发时 需要等待队列首是本线程添加的批任务 才可继续往下执行 否则进行等待
while (!w.done && &w != writers_.front()) {
w.cv.Wait();
}
if (w.done) {
return w.status;
}
// May temporarily unlock and wait.
// 保证有足够空间可写. 该方正常返回一定保证有足够空间可写入数据
Status status = MakeRoomForWrite(my_batch == NULL);
uint64_t last_sequence = versions_->LastSequence();//表示上次已经使用的序列号
Writer* last_writer = &w;
if (status.ok() && my_batch != NULL) { // NULL batch is for compactions
WriteBatch* updates = BuildBatchGroup(&last_writer);//将多个WriteBatch合并成一个WriteBatch
WriteBatchInternal::SetSequence(updates, last_sequence + 1);
last_sequence += WriteBatchInternal::Count(updates); //下一个批任务的起始序号为last_sequence+1
// 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();
//先将record写入到文件中 再将record插入到mem_表中
// 1、Contents方法是返回Slice对象
// 2、此处AddRecord方法调用log_writer.cc中AddRecord方法
status = log_->AddRecord(WriteBatchInternal::Contents(updates));
bool sync_error = false;
if (status.ok() && options.sync) {
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
// 只有在写入文件成功后才 插入到mem_表中
if (status.ok()) {
status = WriteBatchInternal::InsertInto(updates, mem_);
}
mutex_.Lock();
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 (updates == tmp_batch_) tmp_batch_->Clear();
versions_->SetLastSequence(last_sequence);
}
// 生产者消费者模式 通知生产者
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;
}
// Notify new head of write queue
if (!writers_.empty()) {
writers_.front()->cv.Signal();
}
return status;
}
说明:
1) leveldb支持多线程访问db对象,这里采用生产者消费者模式进行同步访问
2) 这里有一个方法MakeRoomForWrite该方法做了很多内容,主要确保有可用内存空间来保证当前WriteBatch写成功。如何保证内存空间可用呢? 例如:将MemTable变成Immutable MemTable以及进行压缩处理。此函数是重点函数,可参考本篇《leveldb深度剖析-压缩流程》
3) 为了提升性能,leveldb采用尽可能多在同一个线程内处理多个WriteBatch,所以会尝试将多个WriteBatch进行合并,具体函数为BuildBatchGroup
4) 根据合并后的WriteBatch分别写入到.log和MemTable中
5) 最后通知生产者
二、写入.log文件
在上面已经提到数据会先写入到.log文件中,然后在写入MemTable中.leveldb中调用log::Writer::AddRecord方法:
/**
* 添加记录
* @param slice 记录内容 实际为string类型
*/
Status Writer::AddRecord(const Slice& slice) {
const char* ptr = slice.data();
size_t left = slice.size();
// Fragment the record if necessary and emit it. Note that if slice
// is empty, we still want to iterate once to emit a single
// zero-length record
Status s;
bool begin = true;
do {
const int leftover = kBlockSize - block_offset_;
assert(leftover >= 0);
if (leftover < kHeaderSize) {
// Switch to a new block 可用空间小于7字节 则启用一个新的block 并且将剩余空间补0
if (leftover > 0) {
// Fill the trailer (literal below relies on kHeaderSize being 7)
assert(kHeaderSize == 7);
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));// PosixWritableFile类对象
}
block_offset_ = 0;
}
// Invariant: we never leave < kHeaderSize bytes in a block.
assert(kBlockSize - block_offset_ - kHeaderSize >= 0);
//可用空间
const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
const size_t fragment_length = (left < avail) ? left : avail; //每次写入大小
RecordType type;
const bool end = (left == fragment_length);
if (begin && end) {// 空间很充足 一次就够
type = kFullType;
} else if (begin) {//需要多次操作 这是一次
type = kFirstType;
} else if (end) {//需要多次操作 这是最后一次
type = kLastType;
} else {
type = kMiddleType;//需要多次操作 这是中间操作
}
s = EmitPhysicalRecord(type, ptr, fragment_length);
ptr += fragment_length;
left -= fragment_length;
begin = false;
} while (s.ok() && left > 0);
return s;
}
函数实现比较简单,注释也非常清晰。如果对log文件组织方式不清楚的,可参考本篇《leveldb深度剖析-存储结构》。
三、总结
截止目前,整个插入流程还是算比较清晰简单,下一篇介绍数据如何插入到MemTable,这部分稍微复杂一点,但也不是特别复杂。