當應用要插入一條記錄時,leveldb首先是将其寫入到log中,若成功,則繼續将其插入到memtable中。是以,當系統故障而memtable又沒有來得及将資料存放到記憶體中,那麼就可以通過log檔案來恢複資料,保證資料不會丢失。
由于log的讀比較複雜,是以将主要介紹log的寫操作。
在class DBImpl中主要有兩個與log相關的成員變量:log::Writer* log_; 和 WritableFile* logfile_;
其中log_用于向logfile_中增加一條記錄 ,logfile_主要用于對log檔案進行同步、重新整理等操作
1、Writer類
class Writer {
public:
explicit Writer(WritableFile* dest);
~Writer();
Status AddRecord(const Slice& slice);
private:
WritableFile* dest_; //以一個WritableFile對象作為Writer的成員,Writer則是将要插入的記錄插入到dest_中
int block_offset_; // 目前位置在Block中的偏移
uint32_t type_crc_[kMaxRecordType + ];//CRC
Status EmitPhysicalRecord(RecordType type, const char* ptr, size_t length);//調用Append寫入資料
};
Writer類對外隻提供了一個方法AddRecord()用于加入一條記錄,同時其中還有一個WritableFile成員變量,記錄最終是插入到WritableFile建立的檔案中的。
根據leveldb源碼可知,log檔案每次都是以32K的實體Block為機關進行操作的,是以log檔案可看作是由很多個連續的32K的Block組成的。插入一條資料時,首先确定資料在Block中的起始位置,然後不斷寫入到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_;//目前Block中的剩餘空間
assert(leftover >= );
if (leftover < kHeaderSize) {//若剩餘空間比固定頭部要小,則要在一個新Block的開始寫入資料
if (leftover > ) {
// Fill the trailer (literal below relies on kHeaderSize being 7)
assert(kHeaderSize == );
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));//尾部填充
}
block_offset_ = ;//塊内偏移置0
}
const size_t avail = kBlockSize - block_offset_ - kHeaderSize;//目前Block中,可用于填充資料的長度
const size_t fragment_length = (left < avail) ? left : avail;//第一個要寫入Block的分段的長度
RecordType type;
const bool end = (left == fragment_length);//若end=1,則表明所有資料都可存放在目前Block中
if (begin && end) {//根據begin和end确定目前記錄的類型type
type = kFullType;
} else if (begin) {
type = kFirstType;
} else if (end) {
type = kLastType;
} else {
type = kMiddleType;
}
s = EmitPhysicalRecord(type, ptr, fragment_length);//将固定頭部和fragment_length長的分段寫入到log檔案dest_中
ptr += fragment_length;//指向資料的指針向前移動已寫入長度
left -= fragment_length;//剩餘待寫入長度減小
begin = false;
} while (s.ok() && left > );
return s;
}
要寫入的記錄分為固定頭部和待寫入資料兩部分,其中固定頭部包括:CRC(4位元組)、記錄長度(2位元組)、type(1位元組)共7位元組。而待寫入資料一般是經過WriteBatch組織的一條記錄(主要包括type(kTypeValue或kTypeDeletion)、key、value)。
2、WritableFile類
class WritableFile {
public:
WritableFile() { }
virtual ~WritableFile();
virtual Status Append(const Slice& data) = ;//寫入記錄
virtual Status Close() = ;//關閉檔案
virtual Status Flush() = ;//重新整理檔案
virtual Status Sync() = ;//同步檔案
};
WritableFile類隻是作為一個抽象基類,定義了一些純虛函數作為接口,最終作為父類被繼承。
leveldb中定義的一個子類為class PosixWritableFile:
class PosixWritableFile : public WritableFile {
private:
std::string filename_;//要操作的檔案名
FILE* file_;//最終要操作的檔案
public:
virtual Status Append(const Slice& data) {
size_t r = fwrite_unlocked(data.data(), , data.size(), file_);//調用fwrite将資料寫入到file_中
return Status::OK();
}
virtual Status Close() {
Status result;
if (fclose(file_) != ) {//關閉檔案
result = IOError(filename_, errno);
}
file_ = NULL;
return result;
}
virtual Status Flush() {
if (fflush_unlocked(file_) != ) {//重新整理檔案
return IOError(filename_, errno);
}
return Status::OK();
}
virtual Status Sync() {//同步檔案
// Ensure new files referred to by the manifest are in the filesystem.
Status s = SyncDirIfManifest();
if (fflush_unlocked(file_) != ||
fdatasync(fileno(file_)) != ) {
s = Status::IOError(filename_, strerror(errno));
}
return s;
}
};
WritableFile類在寫入資料時不會對資料進行任何封裝、修改操作,而是直接将資料寫入到log檔案中。
是以我們一般插入一個Key-Value對時,首先會調用batch.Put(key, value);将其組織成一條記錄,然後調用Write::AddRecord(),在log檔案中找到合适的位置,同時為每一條記錄增加一個頭部,再将其寫入到log檔案中。然後調用WritableFile的Flush()、Sync()等方法來對log檔案進行操作。