@TOC
LevelDB 写入有以下特点
- 单进程多线程:即多个用户同时调用写入接口,将会将写入操作添加队列。
- WAL:日志预写入机制。
- 版本管理: 使用版本管理来保证写入原子性。
写入过程详解
1. 调用LevelDB的写入接口
leveldb::DB *dbptr = nullptr;
// 创建数据库
leveldb::Status s = leveldb::DB::Open(opts, "/tmp/leveldb_test01", &dbptr);
// 调用写入接口
s = db->Put(write_opts, "name", "leveldb");
2. 写入操作进入等待队列
// 代码地址:db/db_impl.cc
writers_.push_back(&w);// 将写入操作添加到writers(双端队列:deque:C)
while (!w.done && &w != writers_.front()) {
w.cv.Wait();// 如果当前的w 不是队列中第一个,并且没有完成就继续
}
3. 为写入操作腾空间
// 代码地址:db/db_impl.cc
Status status = MakeRoomForWrite(updates == nullptr);// 为写入操作腾空间
MakeRoomForWrite 功能
- 判断当前是否满足压缩条件,后台无错误&内存满足限制
- 接下来 解决出现Level0文件数量不超过上限&&后台进程完成压缩操作的一些问题, 如果这些条件都不满足就需要自动触发切换日志文件,版本文件的操作,这样给文件写入留下充足的空间。
// 创建日志文件
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
// 创建版本管理文件
uint64_t new_log_number = versions_->NewFileNumber();
versions_->ReuseFileNumber(new_log_number);
4. 切换版本
WriteBatch* write_batch = BuildBatchGroup(&last_writer);// 构建写批处理组
WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);// 更新序列号
last_sequence += WriteBatchInternal::Count(write_batch); // 更新序列号
5. WAL 预写入日志操作
mutex_.Unlock(); // 先拿锁
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));// 调用日志文件 写入 信息
// 可选择的同步操作
status = logfile_->Sync(); // 将内存中日志数据刷写到磁盘中
6. 写入数据到内存中
status = WriteBatchInternal::InsertInto(write_batch, mem_);// 然后把对应的数据 写入到内存中, 做缓存
7. 如果日志文件同步失败,后台记录这个错误
RecordBackgroundError(status);// 记录背景错误
8. 当前线程成功写入后,使用cv通知其他线程
ready->cv.Signal();
参考源码&项目链接
github 链接: git@github.com:luogaiyu/leveldb.git
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
Writer w(&mute x_); // mutex 主要用于共享资源占有的标记
// 对写入操作进行标记
w.batch = updates;
w.sync = options.sync;
w.done = false;
MutexLock l(&mutex_); // 对当前操作进行上锁
// 将当前的写入操作加入 writer: 双端队列
writers_.push_back(&w);
/**
* 其实是判断当前的写入操作 是否完成
* 当前写入操作是否排的是第一位
* 如果都不是 就加入双端队列中等待
*/
while (!w.done && &w != writers_.front()) {
w.cv.Wait();// 如果当前的操作不满足要求, 就将队列加入 双端队列 等待
}
if (w.done) {
return w.status;// 如果已经完成返回状态
}
// 有可能 短暂解锁并等待
Status status = MakeRoomForWrite(updates == nullptr);// 为写操作 腾出压缩, 具体的原因是为了防止 level0的文件过多
uint64_t last_sequence = versions_->LastSequence();// 使用版本控制获取序列号, 更新版本号的操作,1. 写入操作导致更新,2. 压缩操作导致更新
Writer* last_writer = &w;
if (status.ok() && updates != nullptr) { // 如果扩容操作成功, 并且 批处理为空 说明当前是合并操作 compaction
// -- 版本管理---
WriteBatch* write_batch = BuildBatchGroup(&last_writer);// 构建写批处理组
WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);// 更新序列号
last_sequence += WriteBatchInternal::Count(write_batch); // 更新序列号
// -- 版本管理---
{
mutex_.Unlock();
// -- 向日志文件中新增数据--
// log 指的是 write
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));// 调用日志文件 写入 write_batch的rep_[实际的数据]
// -- 向日志文件中新增数据--
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_);// 然后把对应的数据 写入到内存中, 做缓存
}
mutex_.Lock();// 继续进行上锁
if (sync_error) {
/**
* @brief 因为日志文件的状态不确定
* 强制所有的写入操作都失败
*/
RecordBackgroundError(status);// 记录背景错误
}
}
if (write_batch == tmp_batch_) tmp_batch_->Clear();
versions_->SetLastSequence(last_sequence);
}
// 写请求|担心q
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;
}
// 通知新的写请求队列的头部
if (!writers_.empty()) {
writers_.front()->cv.Signal();
}
return status;
}
猜你喜欢
C++多线程: blog.csdn.net/luog_aiyu/a…
PS
你的赞是我很大的鼓励 欢迎大家加我飞书扩列, 希望能认识一些新朋友~ 二维码见: www.cnblogs.com/DarkChink/p…