1. 概述
2. LSM Tree
2.1 Write Batch
std::string key1,key2,value;
leveldb::Status s;
s = db->Put(leveldb::WriteOptions(), key1, value);
s = db->Delete(leveldb::WriteOptions(), key2);
同時還支持以一種批量的方式寫入數據:std::string key1,key2,value;
leveldb::WriteBatch batch;
batch.Delete(key1);
batch.Put(key2, value);
leveldb::status s = db->Write(leveldb::WriteOptions(), &batch);
2.1 Log Format
每次更新操作都被組織成這樣一個數據包,並作爲一條日誌寫入到log文件中,同時也會被解析爲一條條內存記錄,按照key排序後插入到內存表中的相應位置。LevelDB使用Memory Mapping的方式對log數據進行訪問:如果前一次映射的空間已寫滿,則先將文件擴展一定的長度(每次擴展的長度按64KB,128KB,...的順序逐次翻倍,最大到1MB),然後映射到內存,對映射的內存再以32KB的Page進行切分,每次寫入的日誌填充到Page中,攢積一定量後Sync到磁盤上(也可以設置WriteOptions,每寫一條日誌就Sync一次,但是這樣效率很低),內存映射文件的代碼如下:class PosixMmapFile : public WritableFile
{
private:
std::string filename_; // 文件名稱
int fd_; // 文件句柄
size_t page_size_; //
size_t map_size_; // 內存映射的區域大小
char* base_; // 內存映射區域的起始地址
char* limit_; // 內存映射區域的結束地址
char* dst_; // 最後一次佔用的內存的結束地址
char* last_sync_; // 最後一次同步到磁盤的結束地址
uint64_t file_offset_; // 當前文件的偏移值
bool pending_sync_; // 延遲同步的標誌
public:
PosixMmapFile(const std::string& fname, int fd, size_t page_size)
: filename_(fname),
fd_(fd),
page_size_(page_size),
map_size_(Roundup(65536, page_size)),
base_(NULL),
limit_(NULL),
dst_(NULL),
last_sync_(NULL),
file_offset_(0),
pending_sync_(false) {
assert((page_size & (page_size - 1)) == 0);
}
~PosixMmapFile() {
if (fd_ >= 0) {
PosixMmapFile::Close();
}
}
Status Append(const Slice& data) {
const char* src = data.data();
size_t left = data.size();
while (left > 0) {
// 計算上次最後一次申請的區域的剩餘容量,如果已完全耗盡,
// 則卸載當前區域,申請一個新的區域
size_t avail = limit_ - dst_;
if (avail == 0) {
if (!UnmapCurrentRegion() ||
!MapNewRegion()) {
return IOError(filename_, errno);
}
}
// 填充當前區域的剩餘容量
size_t n = (left <= avail) ? left : avail;
memcpy(dst_, src, n);
dst_ += n;
src += n;
left -= n;
}
return Status::OK();
}
Status PosixMmapFile::Close() {
Status s;
size_t unused = limit_ - dst_;
if (!UnmapCurrentRegion()) {
s = IOError(filename_, errno);
} else if (unused > 0) {
// 關閉時將文件沒有使用用的空間truncate掉
if (ftruncate(fd_, file_offset_ - unused) < 0) {
s = IOError(filename_, errno);
}
}
if (close(fd_) < 0) {
if (s.ok()) {
s = IOError(filename_, errno);
}
}
fd_ = -1;
base_ = NULL;
limit_ = NULL;
return s;
}
virtual Status Sync() {
Status s;
if (pending_sync_) {
// 上個區域也有數據未同步,則先同步數據
pending_sync_ = false;
if (fdatasync(fd_) < 0) {
s = IOError(filename_, errno);
}
}
if (dst_ > last_sync_) {
// 計算未同步數據的起始與結束地址,同步時,起始地址按page_size_向下取整,
// 結束地址向上取整,保證每次同步都是同步一個或多個page
size_t p1 = TruncateToPageBoundary(last_sync_ - base_);
size_t p2 = TruncateToPageBoundary(dst_ - base_ - 1);
// 如果剛好爲整數個page_size_,由於下面同步時必然會加一個page_size_,所以這裏可以減去1
last_sync_ = dst_;
if (msync(base_ + p1, p2 - p1 + page_size_, MS_SYNC) < 0) {
s = IOError(filename_, errno);
}
}
return s;
}
private:
// 將x按y向上對齊
static size_t Roundup(size_t x, size_t y) {
return ((x + y - 1) / y) * y;
}
// 將s按page_size_向下對齊
size_t TruncateToPageBoundary(size_t s) {
s -= (s & (page_size_ - 1));
assert((s % page_size_) == 0);
return s;
}
// 卸載當前映射的內存區域
bool UnmapCurrentRegion() {
bool result = true;
if (base_ != NULL) {
if (last_sync_ < limit_) {
// 如果當前頁沒有完全被同步,則標明本文件需要被同步,下次調用Sync()方法時會將本頁中未同步的數據同步到磁盤
pending_sync_ = true;
}
if (munmap(base_, limit_ - base_) != 0) {
result = false;
}
file_offset_ += limit_ - base_;
base_ = NULL;
limit_ = NULL;
last_sync_ = NULL;
dst_ = NULL; // 使用翻倍的策略增加下次申請區域的大小,最大到1MB
if (map_size_ < (1<<20)) {
map_size_ *= 2;
}
}
return result;
}
bool MapNewRegion() {
assert(base_ == NULL); // 申請一個新的區域時,上一個申請的區域必須已經卸載
// 先將文件擴大
if (ftruncate(fd_, file_offset_ + map_size_) < 0) {
return false;
}
// 將新區域映射到文件
void* ptr = mmap(NULL, map_size_, PROT_READ | PROT_WRITE, MAP_SHARED,
fd_, file_offset_);
if (ptr == MAP_FAILED) {
return false;
}
base_ = reinterpret_cast<char*>(ptr);
limit_ = base_ + map_size_;
dst_ = base_;
last_sync_ = base_;
return true;
}
};
但是,一個Batch的數據按上面的方式組織後,如果做爲一條日誌寫入Log,則很可能需要跨兩個或更多個Page;爲了更好地管理日誌以及保障數據安全,LevelDB對日誌記錄進行了更細的切分,如果一個Batch對應的數據需要跨頁,則會將其切分爲多條Entry,然後寫入到不同Page中,Entry不會跨越Page,我們通過對多個Entry進行解包,可以還原出的Batch數據。最終,LevelDB的log文件被組織爲下面的形式:Status Writer::AddRecord(const Slice& slice) {
const char* ptr = slice.data();
size_t left = slice.size();
Status s;
bool begin = true;
do {
const int leftover = kBlockSize - block_offset_;
assert(leftover >= 0);
if (leftover < kHeaderSize) {
// 如果當前page的剩餘長度小於7字節且大於0,則都填充'\0',並新起一個page
if (leftover > 0) {
assert(kHeaderSize == 7);
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));
}
block_offset_ = 0;
}
// 計算page能否容納整體日誌,如果不能,則將日誌切分爲多條entry,插入不同的page中,type中註明該entry是日誌的開頭部分,中間部分還是結尾部分。
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; // 本Entry保存完整的Batch
} else if (begin) {
type = kFirstType; // 本Entry只保存起始部分
} else if (end) {
type = kLastType; // 本Entry只保存結束部分
} else {
type = kMiddleType; // 本Entry保存Batch的中間部分,不含起始與結尾,有時可能需要保存多個middle
}
s = EmitPhysicalRecord(type, ptr, fragment_length);
ptr += fragment_length;
left -= fragment_length;
begin = false;
} while (s.ok() && left > 0);
return s;
}
Status Writer::EmitPhysicalRecord(RecordType t, const char* ptr, size_t n) {
assert(n <= 0xffff);
assert(block_offset_ + kHeaderSize + n <= kBlockSize);
// 填充記錄頭
char buf[kHeaderSize];
buf[4] = static_cast<char>(n & 0xff);
buf[5] = static_cast<char>(n >> 8);
buf[6] = static_cast<char>(t);
// 計算crc
uint32_t crc = crc32c::Extend(type_crc_[t], ptr, n);
crc = crc32c::Mask(crc);
EncodeFixed32(buf, crc);
// 填充entry內容
Status s = dest_->Append(Slice(buf, kHeaderSize));
if (s.ok()) {
s = dest_->Append(Slice(ptr, n));
if (s.ok()) {
s = dest_->Flush();
}
}
block_offset_ += kHeaderSize + n;
return s;
}
2.3 Write Log Ahead
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates)
{
Status status;
MutexLock l(&mutex_); // 鎖定互斥體,同一時間只能有一個線程更新數據
LoggerId self;
// 獲取Logger的使用權,如果有其他線程擁有所有權,則等待至其釋放所有權。
AcquireLoggingResponsibility(&self);
status = MakeRoomForWrite(false); // May temporarily release lock and wait
uint64_t last_sequence = versions_->LastSequence(); // 獲取當前的版本號
if (status.ok()) {
// 將當前版本號加1後作爲本次更新的日誌的版本,
// 一次批量更新可能包含多個操作,這些操作都用一個版本有一個好處:
// 本次更新的所有操作,要麼都可見,要麼都不可見,不存在一部分可見,另一部分不可見的情況。
WriteBatchInternal::SetSequence(updates, last_sequence + 1);
// 但是本次更新可能有多個操作,跳過與操作數相等的版本號,保證不被使用
last_sequence += WriteBatchInternal::Count(updates);
// 將batch寫入log,然後應用到memtable中
{
assert(logger_ == &self);
mutex_.Unlock();
// 這裏,可以解鎖,因爲在AcquireLoggingResponsibility()方法中已經獲取了Logger的擁有權,
// 其他線程即使獲得了鎖,但是由於&self != logger,其會阻塞在AcquireLoggingResponsibility()方法中。
// 將更新寫入log文件,如果設置了每次寫入進行sync,則將其同步到磁盤,這個操作可能比較長,
// 防止了mutex_對象長期被佔用,因爲其還負責其他一些資源的同步
status = log_->AddRecord(WriteBatchInternal::Contents(updates));
if (status.ok() && options.sync) {
status = logfile_->Sync();
}
if (status.ok()) {
// 成功寫入了log後,才寫入memtable
status = WriteBatchInternal::InsertInto(updates, mem_);
}
// 重新鎖定mutex_
mutex_.Lock();
assert(logger_ == &self);
}
// 更新版本號
versions_->SetLastSequence(last_sequence);
}
// 釋放對logger的所有權,並通知等待的線程,然後解鎖
ReleaseLoggingResponsibility(&self);
return status;
}
// force參數表示強制新起一個memtable
Status DBImpl::MakeRoomForWrite(bool force) {
mutex_.AssertHeld();
assert(logger_ != NULL);
bool allow_delay = !force;
Status s;
while (true) {
if (!bg_error_.ok()) {
// 後臺線程存在問題,則返回錯誤,不接受更新
s = bg_error_;
break;
} else if (
allow_delay &&
versions_->NumLevelFiles(0) >= config::kL0_SlowdownWritesTrigger) {
// 如果不是強制寫入,而且level 0的sstable超過8個,則本次更新阻塞1毫秒,
// leveldb將sstable分爲多個等級,其中level 0中的不同表的key是可能重疊的,
// 如果l0的sstable過多,會導致查詢性能下降,這時需要適當降低更新速度,讓
// 後臺線程進行compaction操作,但是設計者不希望讓某次寫操作等待數秒,
// 而是讓每次更新操作分擔延遲,即每次寫操作阻塞1毫秒,平衡讀寫速率;
// 另外,理論上這也能讓compaction線程獲得更多的cpu時間(當然,
// 這是假定compaction與更新操作共享一個CPU時纔有意義)
mutex_.Unlock();
env_->SleepForMicroseconds(1000);
allow_delay = false; // 最多延遲一次,下次不延遲
mutex_.Lock();
} else if (!force &&
(mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
// 如果當前memtable已使用的空間小於write_buffer_size,則跳出,更新到當前memtable即可。
// 當force爲true時,第一次循環會走後面else邏輯,切換了memtable後force被置爲false,
// 第二次循環時就可以在此跳出了
break;
} else if (imm_ != NULL) {
// 如果當前memtable已經超過write_buffer_size,且備用的memtable也在被使用,則阻塞更新並等待
bg_cv_.Wait();
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
// 如果當前memtable已使用的空間小於write_buffer_size,但是備用的memtable未被使用,
// 則檢查level 0的sstable個數,如超過12個,則阻塞更新並等待
Log(options_.info_log, "waiting...\n");
bg_cv_.Wait();
} else {
// 否則,使用新的id新創建一個log文件,並將當前memtable切換爲備用的memtable,新建一個
// memtable,然後將數據寫入當前的新memtable,即切換log文件與memtable,並告訴後臺線程
// 可以進行compaction操作了
assert(versions_->PrevLogNumber() == 0);
uint64_t new_log_number = versions_->NewFileNumber();
WritableFile* lfile = NULL;
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
if (!s.ok()) {
break;
}
delete log_;
delete logfile_;
logfile_ = lfile;
logfile_number_ = new_log_number;
log_ = new log::Writer(lfile);
imm_ = mem_;
has_imm_.Release_Store(imm_);
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
force = false; // 下次判斷可以不新建memtable了
MaybeScheduleCompaction();
}
}
return s;
}
void DBImpl::AcquireLoggingResponsibility(LoggerId* self) {
while (logger_ != NULL) {
logger_cv_.Wait();
}
logger_ = self;
}
void DBImpl::ReleaseLoggingResponsibility(LoggerId* self) {
assert(logger_ == self);
logger_ = NULL;
logger_cv_.SignalAll();
}
2.4 Skip List
Level DB內部採用跳錶結構來組織Memtable,每插入一條記錄,先根據跳錶通過多次key的比較,定位到記錄應該插入的位置,然後按照一定的概率確定該節點需要建立多少級的索引,跳錶結構如下:
Level DB的SkipList最高12層,最下面一層(level0)的鏈是全鏈,即每條記錄必須在此鏈中插入相應的索引節點;從level1到level11則是按概率決定是否需要建索引,概率按照1/4的因子等比遞減。下面舉個例子,說明一下這個流程:
1. 看上圖,假定我們鏈不存在record3,level0中,record2的下一條記錄是record4,level1中,record2的下一條記錄是record5。
2. 現在,我們插入一條記錄record3,通過key的比較,我們定位到它應該在record2與record4之間。
3. 然後,我們按照下面的代碼確定一條記錄需要在跳錶中建立幾重索引:
template<typename Key, class Comparator>
int SkipList<Key,Comparator>::RandomHeight() {
// Increase height with probability 1 in kBranching
static const unsigned int kBranching = 4;
int height = 1;
while (height < kMaxHeight && ((rnd_.Next() % kBranching) == 0)) {
height++;
}
return height;
}
按照上面的代碼,我們可以得出,建立x級索引的概率是0.25 ^(x - 1) * 0.75,所以,建立1級索引的概率爲75%,建立2級索引的概率爲25%*75%=18.75%,...(個人感覺,google把分支因子定爲4有點高了,這樣在絕大多數情況下,跳錶的高度都不大於3)。
4. 在level0 ~ level (x-1)中鏈表的合適位置插入record3,假定根據上面的公式,我們得到需要爲record3建立2級索引,即x=2,因此需要在level0與level1中的鏈中插入record3:在level 0的鏈中,record3插在record2與record4之間,在level 1的鏈中,record3插入在record2與record5之間,形成了現在的索引結構,在查詢一個記錄時,可以從最高一級索引向下查找,節約比較次數。2.5 Record Format
Level DB將用戶的每個更新或刪除操作組合成一個Record,其格式如下:
從圖中可以看出,每個Record會在原用key的基礎上添加版本號以及key的類型(更新 or 刪除),組成internal key。插入跳錶時,是按照internal key進行排序,而非用戶key。這樣,我們只可能向跳錶中添加節點,而不可能刪除和替換節點。
Internal Key在比較時,按照下面的算法:
int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
if (r == 0) {
// 比較後面8個字節構造的整數,第一個字節的type爲Least Significant Byte
const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
if (anum > bnum) // 注意:整數大反而key比較小
{
r = -1;
} else if (anum < bnum) {
r = +1;
}
}
return r;
}
根據上面的算法,我們可以得知Internal Key的比較順序:
1. 如果User Key不相等,則User Key比較小的記錄的Internal Key也比較小,User Key默認採用字典序(lexicographic)進行比較,可以在建表參數中自定義comparator。
2. 如果type也相同,則比較Sequence Num,Sequence Num大的Internal Key比較小。
3. 如果Sequence Num相等,則比較Type,type爲更新(Key Type=1)的記錄比的type爲刪除(Key Type=0)的記錄的Internal Key小。
在插入到跳錶時,一般不會出現Internal Key相等的情況(除非在一個Batch中操作了同一條記錄兩次,這裏會出現一種bug:在一個Write Batch中,先插入一條記錄,然後刪除這條記錄,最後把這個Batch寫入DB,會發現DB中這條記錄存在。因此,不推薦在Batch中多次操作相同key的記錄),User Key相同的記錄插入跳錶時,Sequence Num大的記錄會排在前面。
設計Internal Key有個以下一些作用:
1. Level DB支持快照查詢,即查詢時指定快照的版本號,查詢出創建快照時某個User Key對應的Value,那麼可以組成這樣一個Internal Key:Sequence=快照版本號,Type=1,User Key爲用戶指定Key,然後查詢數據文件與內存,找到大於等於此Internal Key且User Key匹配的第一條記錄即可(即Sequence Num小於等於快照版本號的第一條記錄)。
2.如果查詢最新的記錄時,將Sequence Num設置爲0xFFFFFFFFFFFFFF即可。因爲我們更多的是查詢最新記錄,所以讓Sequence Num大的記錄排前面,可以在遍歷時遇見第一條匹配的記錄立即返回,減少往後遍歷的次數。
3.文件結構
3.1 文件組成
文件類型 | 說明 |
dbname/MANIFEST-[0-9]+ | 清單文件 |
dbname/[0-9]+.log | db日誌文件 |
dbname/[0-9]+.sst | dbtable文件 |
dbname/[0-9]+.dbtmp | db臨時文件 |
dbname/CURRENT | 記錄當前使用的清單文件名 |
dbname/LOCK | DB鎖文件 |
dbname/LOG | info log日誌文件 |
dbname/LOG.old | 舊的info log日誌文件 |
3. 將臨時文件rename爲CURRENT。
Status SetCurrentFile(Env* env, const std::string& dbname,
uint64_t descriptor_number) {
// 創建一個新的清單文件名
std::string manifest = DescriptorFileName(dbname, descriptor_number);
Slice contents = manifest;
// 移除"dbname/"前綴
assert(contents.starts_with(dbname + "/"));
contents.remove_prefix(dbname.size() + 1);
// 創建一個臨時文件
std::string tmp = TempFileName(dbname, descriptor_number);
// 寫入清單文件名
Status s = WriteStringToFile(env, contents.ToString() + "\n", tmp);
if (s.ok()) {
// 將臨時文件改名爲CURRENT
s = env->RenameFile(tmp, CurrentFileName(dbname));
}
if (!s.ok()) {
env->DeleteFile(tmp);
}
return s;
}
3.2 Manifest
1. Comparator的名稱
void VersionEdit::EncodeTo(std::string* dst) const {
if (has_comparator_) { // 記錄Comparator名稱
PutVarint32(dst, kComdparator);
PutLengthPrefixedSlice(dst, comparator_);
}
if (has_log_number_) { // 記錄Log Numer
PutVarint32(dst, kLogNumber);
PutVarint64(dst, log_number_);
}
if (has_prev_log_number_) { // 記錄Prev Log Number,現在已廢棄,一般爲0
PutVarint32(dst, kPrevLogNumber);
PutVarint64(dst, prev_log_number_);
}
if (has_next_file_number_) { // 記錄下一個文件序號
PutVarint32(dst, kNextFileNumber);
PutVarint64(dst, next_file_number_);
}
if (has_last_sequence_) { // 記錄最大的sequence num
PutVarint32(dst, kLastSequence);
PutVarint64(dst, last_sequence_);
}
// 記錄每一級Level下次compaction的起始Key
for (size_t i = 0; i < compact_pointers_.size(); i++) {
PutVarint32(dst, kCompactPointer);
PutVarint32(dst, compact_pointers_[i].first); // level
PutLengthPrefixedSlice(dst, compact_pointers_[i].second.Encode());
}
// 記錄每一級需要刪除的文件
for (DeletedFileSet::const_iterator iter = deleted_files_.begin();
iter != deleted_files_.end();
++iter) {
PutVarint32(dst, kDeletedFile);
PutVarint32(dst, iter->first); // level
PutVarint64(dst, iter->second); // file number
}
// 記錄每一級需要有效的sst以及其smallest與largest的key
for (size_t i = 0; i < new_files_.size(); i++) {
const FileMetaData& f = new_files_[i].second;
PutVarint32(dst, kNewFile);
PutVarint32(dst, new_files_[i].first); // level
PutVarint64(dst, f.number);
PutVarint64(dst, f.file_size);
PutLengthPrefixedSlice(dst, f.smallest.Encode());
PutLengthPrefixedSlice(dst, f.largest.Encode());
}
}
3.3 Sortedtable
3.4 Sparse Index
void BytewiseComparatorImpl::FindShortestSeparator(
std::string* start,
const Slice& limit) const {
// 先比較獲得最大公共前綴
size_t min_length = std::min(start->size(), limit.size());
size_t diff_index = 0;
while ((diff_index < min_length) &&
((*start)[diff_index] == limit[diff_index])) {
diff_index++;
}
if (diff_index >= min_length) {
// 如果start就是limit的前綴,則只能使用start本身作爲分割
} else {
uint8_t diff_byte = static_cast<uint8_t>((*start)[diff_index]);
// 將第一個不同字符+1,並確保其不會溢出,同時比limit小
if (diff_byte < static_cast<uint8_t>(0xff) &&
diff_byte + 1 < static_cast<uint8_t>(limit[diff_index])) {
(*start)[diff_index]++;
start->resize(diff_index + 1);
assert(Compare(*start, limit) < 0);
}
}
}
從上面可以看出,FindShortestSeparator方法並不嚴格,有些時候沒有找出最短分割的key(比如第一個不等的字符已經爲0xFF時),它只是一種優化,我們自定義Comparator時,既可以實現,也可以不實現,如果不實現,將始終使用Data Block的最大Key作爲索引,並不影響功能正確性。4. Operations
4.1 創建一個新表
創建一個新的表大概分爲幾步,包括建立各類文件以及內存中的數據結構,線程同步對象等,關鍵代碼如下:
// DBImpl在構造時會初始化互斥體與信號量,創建一個空的memtable,並根據配置設置Comparator及LRU緩衝
DBImpl::DBImpl(const Options& options, const std::string& dbname)
: env_(options.env),
internal_comparator_(options.comparator), // 初始化Comparator
options_(SanitizeOptions(dbname, &internal_comparator_, options)), // 檢查參數是否合法
owns_info_log_(options_.info_log != options.info_log), // 是擁有自己info log,還是使用用戶提供的
owns_cache_(options_.block_cache != options.block_cache), // 是否擁有自己的LRU緩衝,或者使用用戶提供的
dbname_(dbname), // 數據表名稱
db_lock_(NULL), // 不創建也不鎖定文件鎖
shutting_down_(NULL),
bg_cv_(&mutex_), // 用於與後臺線程交互的條件信號
mem_(new MemTable(internal_comparator_)), // 創建一個新的跳錶
imm_(NULL), // 用於雙緩衝的緩衝跳錶開始時爲NULL
logfile_(NULL), // log文件
logfile_number_(0), // log文件的序號
log_(NULL), // log writer
logger_(NULL), // 用於在多線程環境中記錄Owner logger的一個指針
logger_cv_(&mutex_), // 用於與Logger交互的條件信號
bg_compaction_scheduled_(false), // 沒打開表時不起動後臺的compaction線程
manual_compaction_(NULL) {
// 增加memtable的引用計數
mem_->Ref();
has_imm_.Release_Store(NULL);
// 根據Option創建一個LRU的緩衝對象,如果options中指定了Cache空間,則使用用戶
// 提供的Cache空間,否則會在內部確實創建8MB的Cache,另外,LRU的Entry數目不能超過max_open_files-10
const int table_cache_size = options.max_open_files - 10;
table_cache_ = new TableCache(dbname_, &options_, table_cache_size);
// 創建一個Version管理器
versions_ = new VersionSet(dbname_, &options_, table_cache_,
&internal_comparator_);
}
Options SanitizeOptions(const std::string& dbname,
const InternalKeyComparator* icmp,
const Options& src) {
Options result = src;
result.comparator = icmp;
ClipToRange(&result.max_open_files, 20, 50000);
ClipToRange(&result.write_buffer_size, 64<<10, 1<<30);
ClipToRange(&result.block_size, 1<<10, 4<<20);
// 如果用戶未指定info log文件(用於打印狀態等文本信息的日誌文件),則由引擎自己創建一個info log文件。
if (result.info_log == NULL) {
// Open a log file in the same directory as the db
src.env->CreateDir(dbname); // 如果目錄不存在則創建
// 如果已存在以前的info log文件,則將其改名爲LOG.old,然後創建新的log文件與日誌的writer
src.env->RenameFile(InfoLogFileName(dbname), OldInfoLogFileName(dbname));
Status s = src.env->NewLogger(InfoLogFileName(dbname), &result.info_log);
if (!s.ok()) {
result.info_log = NULL;
}
}
// 如果用戶沒指定LRU緩衝,則創建8MB的LRU緩衝
if (result.block_cache == NULL) {
result.block_cache = NewLRUCache(8 << 20);
}
return result;
}
Status DBImpl::NewDB() {
// 創建version管理器
VersionEdit new_db;
// 設置Comparator
new_db.SetComparatorName(user_comparator()->Name());
new_db.SetLogNumber(0);
// 下一個序號從2開始,1留給清單文件
new_db.SetNextFile(2);
new_db.SetLastSequence(0);
// 創建一個清單文件,MANIFEST-1
const std::string manifest = DescriptorFileName(dbname_, 1);
WritableFile* file;
Status s = env_->NewWritableFile(manifest, &file);
if (!s.ok()) {
return s;
}
{
// 寫入清單文件頭
log::Writer log(file);
std::string record;
new_db.EncodeTo(&record);
s = log.AddRecord(record);
if (s.ok()) {
s = file->Close();
}
}
delete file;
if (s.ok()) {
// 設置CURRENT文件,使其指向清單文件
s = SetCurrentFile(env_, dbname_, 1);
} else {
env_->DeleteFile(manifest);
}
return s;
4.2 打開一個已存在的表
Status DB::Open(const Options& options, const std::string& dbname,
DB** dbptr) {
*dbptr = NULL;
DBImpl* impl = new DBImpl(options, dbname);
impl->mutex_.Lock();
VersionEdit edit;
// 如果存在表數據,則Load表數據,並對日誌進行恢復,否則,創建新表
Status s = impl->Recover(&edit);
if (s.ok()) {
// 從VersionEdit獲取一個新的文件序號,所以如果是新建數據表,則第一個LOG的序號爲2(1已經被MANIFEST佔用)
uint64_t new_log_number = impl->versions_->NewFileNumber();
// 記錄日誌文件號,創建新的log文件及Writer對象
WritableFile* lfile;
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);
// 如果存在原來的log,則回放log
s = impl->versions_->LogAndApply(&edit, &impl->mutex_);
}
if (s.ok()) {
// 刪除廢棄的文件(如果存在)
impl->DeleteObsoleteFiles();
// 檢查是否需要Compaction,如果需要,則讓後臺啓動Compaction線程
impl->MaybeScheduleCompaction();
}
}
impl->mutex_.Unlock();
if (s.ok()) {
*dbptr = impl;
} else {
delete impl;
}
return s;
}
從上面可以看出,其實到底是新建表還是打開表都是取決與DBImpl::Recover()這個方法的行爲,它的流程如下:Status DBImpl::Recover(VersionEdit* edit) {
mutex_.AssertHeld();
// 創建DB目錄,不關注錯誤
env_->CreateDir(dbname_);
// 在DB目錄下打開或創建(如果不存在)LOCK文件並鎖定它,防止其他進程打開此表
Status s = env_->LockFile(LockFileName(dbname_), &db_lock_);
if (!s.ok()) {
return s;
}
if (!env_->FileExists(CurrentFileName(dbname_))) {
// 如果DB目錄下不存在CURRENT文件且允許在表不存在時創建表,則新建一個表返回
if (options_.create_if_missing) {
s = NewDB();
if (!s.ok()) {
return s;
}
} else {
return Status::InvalidArgument(
dbname_, "does not exist (create_if_missing is false)");
}
} else {
if (options_.error_if_exists) {
return Status::InvalidArgument(
dbname_, "exists (error_if_exists is true)");
}
}
// 如果運行到此,表明表已經存在,需要load,第一步是從MANIFEST文件中恢復VersionSet
s = versions_->Recover();
if (s.ok()) {
SequenceNumber max_sequence(0);
// 獲取MANIFEST中獲取最後一次持久化清單時在使用LOG文件序號,注意:這個LOG當時正在使用,
// 表明數據還在memtable中,沒有形成sst文件,所以數據恢復需要從這個LOG文件開始(包含這個LOG)。
// 另外,prev_log是早前版本level_db使用的機制,現在以及不再使用,這裏只是爲了兼容
const uint64_t min_log = versions_->LogNumber();
const uint64_t prev_log = versions_->PrevLogNumber();
// 掃描DB目錄,記錄下所有比MANIFEST中記錄的LOG更加新的LOG文件
std::vector<std::string> filenames;
s = env_->GetChildren(dbname_, &filenames);
if (!s.ok()) {
return s;
}
uint64_t number;
FileType type;
std::vector<uint64_t> logs;
for (size_t i = 0; i < filenames.size(); i++) {
if (ParseFileName(filenames[i], &number, &type)
&& type == kLogFile
&& ((number >= min_log) || (number == prev_log))) {
logs.push_back(number);
}
}
// 將LOG文件安裝從小到大排序
std::sort(logs.begin(), logs.end());
// 逐個LOG文件回放 for (size_t i = 0; i < logs.size(); i++) {
// 回放LOG時,記錄被插入到memtable,如果超過write buffer,則還會dump出level 0的sst文件,
// 此方法會將日誌種每條記錄的sequence num與max_sequence進行比較,以記錄下最大的sequence num。
s = RecoverLogFile(logs[i], edit, &max_sequence);
// 更新最大的文件序號,因爲MANIFEST文件中沒有記錄這些LOG文件佔用的序號;
// 當然,也可能LOG的序號小於MANIFEST中記錄的最大文件序號,這時不需要更新。
versions_->MarkFileNumberUsed(logs[i]);
}
if (s.ok()) {
// 比較日誌回放前後的最大sequence num,如果回放記錄中有超過LastSequence()的記錄,則替換
if (versions_->LastSequence() < max_sequence) {
versions_->SetLastSequence(max_sequence);
}
}
}
return s;
}
4.3 關閉一個已打開的表
DBImpl::~DBImpl() {
// 通知後臺線程,DB即將關閉
mutex_.Lock();
// 後臺線程會間歇性地檢查shutting_down_對象的指針,一旦不爲NULL就會退出
shutting_down_.Release_Store(this);
// 注意:這裏必須循環通知,直至compaction線程獲得信號並設置了bg_compaction_scheduled_爲false
while (bg_compaction_scheduled_) {
bg_cv_.Wait();
}
mutex_.Unlock();
// 如果鎖定了文件鎖,則釋放文件鎖
if (db_lock_ != NULL) {
env_->UnlockFile(db_lock_);
}
delete versions_;
// 減去memtable的引用計數
if (mem_ != NULL) mem_->Unref();
if (imm_ != NULL) imm_->Unref();
// 銷燬db log相關對象以及表緩衝對象
delete log_;
delete logfile_;
delete table_cache_;
// 如果info log與cache是引擎自己構建,則需要銷燬它們
if (owns_info_log_) {
delete options_.info_log;
}
if (owns_cache_) {
delete options_.block_cache;
}
}
由上可見,delete一個db對象可能會阻塞調用線程一段時間,必須讓其完成一些必須完成的工作,才能進一步保障數據的安全。