实现跨平台的env类
graph TD
B[env抽象类] --> C{操作系统宏定义}
C -->|Win| D[env_win]
C -->|POSIX| E[env_posix]
抽象类env:
delete拷贝构造函数和拷贝赋值函数
static Env* Default()用于静态的构造env对象
其他方法主要是于操作系统交互的方法,比如新建文件,删除文件,删除文件夹等,都是virtual修饰,需要子类自己实现
注意:一般函数返回是状态码的话,如果还有要回传的比如指针这些就放在形参里面
Put接口写入过程
写入过程
leveldb提供了put接口供写入kv
leveldb写入的记录是通过构造成writebatch然后调用write写入的
多线程写入在write中通过合并写只写入一次,优化了写性能,本质上是通过构造一个抽象的阻塞队列来限制只有一个writer对象(即一个写线程)执行写,该线程在写入之前会遍历所有写对象要写入的内容并合并,最后执行一次写,然后依次唤醒其他线程并返回状态。
在实现方面主要有几个点:
-
构建一个队列用于存放writer对象,并且通过mutex来限制对队列的互斥访问
-
每个writer都有对应的信号量,当不是队列front的时候就释放mutex并阻塞等待信号量唤醒
-
当被唤醒后就检查是否已经done了,done了返回状态,没有继续写
-
正常写入时遍历整个队列的writer对象,要把写入的内容全都合并,一次性写入即可
-
写入完成后,依次唤醒阻塞的writer
-
要点:不需要全流程阻塞,合并所有统一写入的writer的内容后,标记一下合并写入的最后一个writer,就可以释放mutex,供新的writer进来,在唤醒时,通过标记的最后一个writer为终止,依次唤醒即可
需求:多线程访问相同key,如何实现本地阻塞队列限制仅一个线程进入去访问目标主机?
实现:
-
构建一个map,用一个mutex限制对map的访问, map的key为string类型,value为一个结构体{val,error,waitgroup},waitgroup跟信号量类似
-
每个reader需要先获取mutex,然后添加判断map中是否存在对应key的value了,如果存在则直接调用对应的waitgroup阻塞等待唤醒
-
如果没有,则添加value,然后释放mutex,供其他reader访问,然后自己去读远程节点的信息,返回结果后,唤醒阻塞在该信号量的所有reader,并返回读取到的结果,然后就可以加锁把map中的key删除掉了
流程图
graph TD
A[writer获取mutex阻塞直到获取到mutex] --> B(添加到writers队列中)
B --> C{当前w未完成写并且不是队列头}
C -->|是| D[休眠释放mutex等待被唤醒]
D --> |被唤醒后| Q{是否已经done了.如果被合并写后done会被置位true}
Q --> |是| W[返回状态]
Q --> |否| E[正常写入]
C -->|否| E
E --> F{检测是否有足够的空间}
F --> |是| G[1.遍历writers中所有writer合并所有写入的内容到一个writerbatch 2.写 3. 更新合并写的所有writer的状态并唤醒 4.唤醒新的一个writer]
F --> |否| H[会休眠1ms.等待compaction]
Put源码剖析
db_impl.h中的DB_Impl类,继承自DB
成员变量:
mutex //更新所有状态或writers队列时都必须先获取该互斥锁
writers //deque<Writer>队列,写入
db_impl.cc Writer类
// Information kept for every waiting writer
struct DBImpl::Writer {
explicit Writer(port::Mutex* mu)
: batch(nullptr), sync(false), done(false), cv(mu) {}
Status status; //返回状态
WriteBatch* batch; //写入对象,leveldb把写入内容封装成该对象
bool sync; //用于判断是否需要刷盘
bool done; //用于判断是否执行完毕
port::CondVar cv; //条件变量
};
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
Writer w(&mutex_);
w.batch = updates;
w.sync = options.sync; //默认是false
w.done = false;
//RAII的思想,构造即初始化,即构造的时候上锁,析构的时候解锁
//writers_是全局唯一的队列deque<Writer>,使用该队列需要先加锁,锁也是全局唯一的
MutexLock l(&mutex_);
writers_.push_back(&w);
//如果未写入并且不是队列头则阻塞等待
//这里应该是构建了一个阻塞队列,只放一个进去写,其他的等待,然后写入后再依次唤醒其他阻塞
//writer,直接返回状态即可
while (!w.done && &w != writers_.front()) {
w.cv.Wait();//Wait会释放mutex
}
//如果已经done了则返回状态
if (w.done) {
return w.status;
}
//所以只有未写入且属于队列头的writer才能执行以下逻辑
// May temporarily unlock and wait.
//MakeRoomForWrite 清理空间及各方条件合适后供写入
Status status = MakeRoomForWrite(updates == nullptr);
uint64_t last_sequence = versions_->LastSequence();
Writer* last_writer = &w; //w是当前的deque.front(),last_writer初始化为头,在BuildBatchGroup中会逐步被更新为最后一个writer
if (status.ok() && updates != nullptr) { // nullptr batch is for compactions
//这里的逻辑是把所有其他wirter的batch都合并到一起,合并写只需要写入一次
WriteBatch* write_batch = BuildBatchGroup(&last_writer);
WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);
last_sequence += WriteBatchInternal::Count(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,这里为什么要释放?
//释放了的话,如果有新的writer就可以加入到writers中
//这里有新的writer加入到writers也没有关系,因为已经记录了当前被合并写的最后一个writer了,为last_writer
//在后续的唤醒操作时,只唤醒到last_writer即可,这个也算是神来之笔了
mutex_.Unlock();
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
bool sync_error = false;
if (status.ok() && options.sync) {
//Sync是刷盘
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
if (status.ok()) {
//写入到mem中
status = WriteBatchInternal::InsertInto(write_batch, 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 (write_batch == tmp_batch_) tmp_batch_->Clear();
versions_->SetLastSequence(last_sequence);
}
//依次唤醒队列中阻塞的所有writer
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
}
// 注意需要重新唤醒一个新的writer,因为中间释放了锁,所以有新的writer进来并且因为不是front而被休眠
// Notify new head of write queue
if (!writers_.empty()) {
writers_.front()->cv.Signal();
}
return status;
}
MakeRoomForWrite逻辑
// REQUIRES: mutex_ is held
// REQUIRES: this thread is currently at the front of the writer queue
Status DBImpl::MakeRoomForWrite(bool force) {
mutex_.AssertHeld();
assert(!writers_.empty());
bool allow_delay = !force;
Status s;
//注意这里是while(true),所以如果break才会退出,如果没有break还会重新判断
/*
1、允许延迟并且当前level0文件数量超过了规定的缓速数量,则释放mutex(释放后允许新的writer进入writers,但这些新的writer因为不是front所以仍然会休眠),
然后本线程睡1ms,并且设置不允许再休眠了,醒后重新获取锁,虽然重新进入循环判断。这样做的好处是文件数量太多的情况下,把CPU资源腾出来给compaction线程,尽量把文件数降低。
2、 如果非强制并且当前内存使用量小于分配量,说明现在内存有空间,所以直接break返回即可。updates==nullptr->force为true
3、 imm不为nullptr,说明mem满了转换成immumem了,阻塞等地后天compaction完成,再写入
4、 如果当前level0文件已经达到规定的暂停触发数量,则休眠等待compaction完成,注意这个值跟1的缓速数量的区别
5、 其他情况,说明当前mem没有内存,也没有imm,后台也没有在compaction,则当前是初始化状态,则创建log,创建mem,新建后台线程负责后台compaction
*/
while (true) {
//bg_error_是一个db对象的状态变量
if (!bg_error_.ok()) {
// Yield previous error
s = bg_error_;
break;
} else if (allow_delay && versions_->NumLevelFiles(0) >=
config::kL0_SlowdownWritesTrigger) {
//如果允许延迟并且当前的level0文件数大于等于最大限制数
//此时会延迟写入,而是把CPU让给合并线程,从而减少文件数量
//解锁,休眠1ms,每个线程最多休眠一次
// 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.
//写入接近硬件限制最大的LO文件数量了。
//当达到硬件限制时,不是延迟单个writer几秒,而是对每个writer都延迟1ms,从而减少延迟差异
//如果共享同个CPU核心,这样延迟可以把cpu资源让给合并县城
mutex_.Unlock();
env_->SleepForMicroseconds(1000);
allow_delay = false; // Do not delay a single write more than once
mutex_.Lock();
} else if (!force &&
(mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
// There is room in current memtable
//如果非强制写入,。。。
break;
} else if (imm_ != nullptr) {
// We have filled up the current memtable, but the previous
// one is still being compacted, so we wait.
//如果当前有immu内存表对象,即当前已经填满了一个,在生成另一个mem,则等待
Log(options_.info_log, "Current memtable full; waiting...\n");
background_work_finished_signal_.Wait();//db对象唯一条件变量,应该在更新完mem后会唤醒
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
// There are too many level-0 files.
//此时是不允许延迟了,但仍然有太多level0文件,则休眠,等合并完被唤醒
Log(options_.info_log, "Too many L0 files; waiting...\n");
background_work_finished_signal_.Wait();
} else {
// Attempt to switch to a new memtable and trigger compaction of old
//尝试选择一个新的mem表并且触发旧表的minor合并操作
assert(versions_->PrevLogNumber() == 0);//断言没有log文件则执行下面逻辑
uint64_t new_log_number = versions_->NewFileNumber();//申请一个新的文件号
WritableFile* lfile = nullptr;
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);//申请一个新的log文件
if (!s.ok()) {
// Avoid chewing through file number space in a tight loop.
versions_->ReuseFileNumber(new_log_number);
break;
}
//删除日志文件写对象
delete log_;
delete logfile_;
//重新复制日志写对象
logfile_ = lfile;
logfile_number_ = new_log_number;
log_ = new log::Writer(lfile);
//mem转immutable_mem
imm_ = mem_;
//原子量,用了释放写,让其他线程知道
has_imm_.store(true, std::memory_order_release);
//重新申请mem
mem_ = new MemTable(internal_comparator_);
//引用+1
mem_->Ref();
force = false; // Do not force another compaction if have room
MaybeScheduleCompaction();
}
}
return s;
}
创建compaction的逻辑
/*
后台线程是一个队列,只要队列里面有后台线程,就会依次拿出来执行,执行完后就删除
所以compaction的核心逻辑BGWork里面首先执行了compaction,然后再重新构建一个线程加入到后台线程队列中
从而实现的不断的compaction
*/
void DBImpl::MaybeScheduleCompaction() {
mutex_.AssertHeld();
if (background_compaction_scheduled_) {
// Already scheduled
//当前已经在compaction
} else if (shutting_down_.load(std::memory_order_acquire)) {
// DB is being deleted; no more background compactions
} else if (!bg_error_.ok()) {
// Already got an error; no more changes
} else if (imm_ == nullptr && manual_compaction_ == nullptr &&
!versions_->NeedsCompaction()) {
// No work to be done
} else {
//标志位设置为true,表示执行后台合并中,然后创建一个后台线程用于执行compaction,加入到后台线程队列中
background_compaction_scheduled_ = true;
//DBImpl::BGWork就是执行逻辑,一个静态函数,this是参数
env_->Schedule(&DBImpl::BGWork, this);
}
}
void DBImpl::BGWork(void* db) {
reinterpret_cast<DBImpl*>(db)->BackgroundCall();
}
void DBImpl::BackgroundCall() {
MutexLock l(&mutex_);
assert(background_compaction_scheduled_);
if (shutting_down_.load(std::memory_order_acquire)) {
// No more background work when shutting down.
} else if (!bg_error_.ok()) {
// No more background work after a background error.
} else {
BackgroundCompaction();
}
background_compaction_scheduled_ = false;
// Previous compaction may have produced too many files in a level,
// so reschedule another compaction if needed.
MaybeScheduleCompaction();
background_work_finished_signal_.SignalAll();
}