本章主要介紹對LevelDB基礎操作Open,Put,Get等操作,熟悉具體的數據讀寫處理流程。
先放一個基本的LevelDB的基礎操作demo。demo實現的就是首先打開一個數據庫,然後向其插入一個KV數據,隨後讀取出來。
int main(int argc,char* argv[]){
leveldb::DB* ptr = nullptr;
const std::string name = "./myleveldata.ldb";
leveldb::Options option;
option.create_if_missing = true;
//open
leveldb::Status status = leveldb::DB::Open(option,name,&ptr);
if(!status.ok()){
std::cerr<<"open db error "<<status.ToString()<<std::endl;
return 1;
}
assert(ptr != nullptr);
const std::string key = "myfirstkey";
const std::string value = "myfirstvalue";
//put
{
leveldb::WriteOptions writeoptions;
writeoptions.sync = true; //leveldb默認寫是異步的,這裏打開syncx同步
status = ptr->Put(writeoptions,key,value);
if(!status.ok()){
std::cerr<<"write error";
return 1;
}
}
//get
{
leveldb::ReadOptions getOptions;
std::string rdVal;
status = ptr->Get(getOptions, key, &rdVal);
if (!status.ok()) {
std::cerr << "get data error" << std::endl;
return 0;
}
assert(value == rdVal);
std::cout<<"happy ending ";
}
delete ptr;
return 0;
}
可以看出,整體數據操作都是圍繞DB*,首先在打開數據庫的時候會返回一個DB*,然後圍繞其做增刪改查操作。DB類是一個抽象類,但是其對於純虛函數都有提供默認實現,繼承其的子類也還是要重寫這些虛函數。
class LEVELDB_EXPORT DB {
public:
// 以下函數具體實現在db_impl.cc
// 打開數據庫,返回dbptr
static Status Open(const Options& options, const std::string& name,
DB** dbptr);
DB() = default;
DB(const DB&) = delete;
DB& operator=(const DB&) = delete;
virtual ~DB();
// 注意這裏純虛函數,也可以提供默認實現
// 向數據庫中插入key-value對
virtual Status Put(const WriteOptions& options, const Slice& key,
const Slice& value) = 0;
// 數據庫中刪除key
virtual Status Delete(const WriteOptions& options, const Slice& key) = 0;
// 數據庫更新
virtual Status Write(const WriteOptions& options, WriteBatch* updates) = 0;
// 數據庫中讀取key,如果有則返回value,如果沒有則保持value不變,status=Status::IsNotFount()
virtual Status Get(const ReadOptions& options, const Slice& key,
std::string* value) = 0;
...
};
數據庫Open流程
Open流程較爲容易,簡單而言就是根據輸入的dbname,找到對應的磁盤文件,將其數據恢復到內存中,同時設置log和manifest等,將這些數據都放到DBimpl*對象中,並返回其爲進一步的讀寫操作。
Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) {
*dbptr = nullptr;
//構造一個impl,並調用Recover
DBImpl* impl = new DBImpl(options, dbname);
//接下來的操作是從日誌中恢復數據庫的內容(將數據加載到內存中),因爲裏面要對impl進行賦值,所以要先加鎖
impl->mutex_.Lock();
VersionEdit edit;
// Recover handles create_if_missing, error_if_exists
bool save_manifest = false;
Status s = impl->Recover(&edit, &save_manifest);
if (s.ok() && impl->mem_ == nullptr) {
// Create new log and a corresponding memtable.
// 創建新log和memtable(並且要自己增加reference)
uint64_t new_log_number = impl->versions_->NewFileNumber();
WritableFile* lfile;
// 將數據從磁盤文件中加載出來賦給impl
s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
&lfile);
if (s.ok()) {
edit.SetLogNumber(new_log_number);
impl->logfile_ = lfile;
impl->logfile_number_ = new_log_number;
impl->log_ = new log::Writer(lfile);
impl->mem_ = new MemTable(impl->internal_comparator_);
impl->mem_->Ref();
}
}
if (s.ok() && save_manifest) {
edit.SetPrevLogNumber(0); // No older logs needed after recovery.
edit.SetLogNumber(impl->logfile_number_);
// 設置Manifest爲當前版本
s = impl->versions_->LogAndApply(&edit, &impl->mutex_);
}
if (s.ok()) {
impl->DeleteObsoleteFiles();
impl->MaybeScheduleCompaction();
}
impl->mutex_.Unlock();
if (s.ok()) {
assert(impl->mem_ != nullptr);
*dbptr = impl;
} else {
delete impl;
}
return s;
}
數據庫Put、Delete操作
put操作是將key-value插入到數據庫中。首先會調用DBImpl::Put函數,而DBImpl::Put直接調用父類的DB::Put ,DB::Put中新建一個WriteBatch對象,調用WriteBatch::Put函數,最後在調用DBImpl::Write函數。下圖所示的是一個Batch對象的結構。
對於WriteBatch對象的構建都是在WriteBatch::Put函數中進行的,函數的作用就是將key和value值都放在batch對象中。這裏有一點需要說明的是,數據庫的寫入操作和刪除操作其實是相似的操作,delete操作其實就是一個value爲空的put操作,寫入操作的type是kTypeValue,刪除操作的type是kTypeDeletion。
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
WriteBatch batch;
batch.Put(key, value);
return Write(opt, &batch);
}
Status DB::Delete(const WriteOptions& opt, const Slice& key) {
WriteBatch batch;
batch.Delete(key);
return Write(opt, &batch);
}
接下來的DBImlp::Write函數操作至關重要,這個函數的作用是具體寫入,其能體現出LevelDB寫性能卓越的原因。具體過程如下:
- 首先初始化一個Writer對象,Writer對象其實是batch對象的一個封裝數據結構。
- 然後用一個deque隊列同步線程,目的是合併多個batch一起寫入磁盤,將多個操作合併成一個批插入操作,這樣能有效減少寫磁盤的次數,提高系統的寫性能。這裏還有一個小技巧,利用條件變量和互斥量來實現這個功能。不同的線程將Writer對象插入deque隊列,如果發現隊列中已經存在Writer對象(也就是說對象非隊列頭部)則wait等待。直到等到信號纔會被一起BuildBatchGroup寫入log和MemTable。這裏給一個例子。這裏有五個線程,A,B,C,D,E,每個線程都擁有一個Writer想要寫入。一開始deque隊列爲空,A線程獲取mutex,然後deque中push進去,因爲其爲隊列首部,則繼續執行下去,由於其一直佔着mutex,其他線程無法插入到deque中,所以線程A執行的BuildBatchGroup,SetSequence等操作都是隻包含自己,然而在寫日誌之前,線程A釋放掉mutex,這時候其他線程就可以爭搶mutex並加入deque中,但是由於並不是隊列頭部而條件變量等待,這時候又會釋放掉mutex,其他線程繼續爭搶,最後B,C,D,E都會加入deque,但是順序很可能不一樣。接着線程A會繼續寫入log和MemTable,然後會調用signal喚醒隊列中下一個線程(假設爲C),線程C喚醒後,也是同樣的操作,只是此時隊列中還剩下B,D,E線程,這時候BuildBatchGroup就可以將deque中的所有Writer合併爲一個batch,一次性的寫入log和MemTable,最後循環pop隊列頭部元素,並比較是否爲last_writer,比較完成(E == last_writer)後即退出。
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
//初始化Writer
Writer w(&mutex_);
w.batch = updates;
w.sync = options.sync;
w.done = false;
//加鎖爲了線程同步(條件變量),合併多個batch一起寫入磁盤,合併爲一個批插入操作,這也是寫操作高性能的關鍵所在
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(updates == nullptr);
uint64_t last_sequence = versions_->LastSequence();
Writer* last_writer = &w;
if (status.ok() && updates != nullptr) { // nullptr batch is for compactions
//將writers_隊列中的所有batch合併,一起寫入。
WriteBatch* updates = BuildBatchGroup(&last_writer);
WriteBatchInternal::SetSequence(updates, last_sequence + 1);
last_sequence += WriteBatchInternal::Count(updates);
// 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();
//數據順序寫入log日誌
status = log_->AddRecord(WriteBatchInternal::Contents(updates));
bool sync_error = false;
if (status.ok() && options.sync) {
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
if (status.ok()) {
//數據插入到memTable
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;
}
數據庫Get操作
Get操作也是很重要的一塊,雖然主體流程很清晰,但是裏面有很多需要注意的點,花費了工程師很多時間優化。比如這裏的LookupKey的作用,還有在MemTable中查找的過程等。下一章我會分析MemTable和Version的結構、讀寫過程。
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
MemTable* mem = mem_;
//Immutable MemTable
MemTable* imm = imm_;
//current version
Version* current = versions_->current();
//要自己增加引用計數
mem->Ref();
if (imm != nullptr) imm->Ref();
current->Ref();
bool have_stat_update = false;
Version::GetStats stats;
// Unlock while reading from files and memtables
{
mutex_.Unlock();
// First look in the memtable, then in the immutable memtable (if any).
// 讀取順序,Memtable -> Immutable Memtable -> Current Version
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();
}
//如果是從磁盤中獲取的數據,則有可能出發Comapction合併操作
if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction();
}
// 讀取完成後引用計數減一
mem->Unref();
if (imm != nullptr) imm->Unref();
current->Unref();
return s;
}
參考博客:
- https://leveldb-handbook.readthedocs.io/zh/latest/rwopt.html
- https://www.cnblogs.com/ym65536/p/7720105.html