当应用要插入一条记录时,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 + 1];//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 >= 0);
if (leftover < kHeaderSize) {//若剩余空间比固定头部要小,则要在一个新Block的开始写入数据
if (leftover > 0) {
// Fill the trailer (literal below relies on kHeaderSize being 7)
assert(kHeaderSize == 7);
dest_->Append(Slice("\x00\x00\x00\x00\x00\x00", leftover));//尾部填充
}
block_offset_ = 0;//块内偏移置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 > 0);
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) = 0;//写入记录
virtual Status Close() = 0;//关闭文件
virtual Status Flush() = 0;//刷新文件
virtual Status Sync() = 0;//同步文件
};
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(), 1, data.size(), file_);//调用fwrite将数据写入到file_中
return Status::OK();
}
virtual Status Close() {
Status result;
if (fclose(file_) != 0) {//关闭文件
result = IOError(filename_, errno);
}
file_ = NULL;
return result;
}
virtual Status Flush() {
if (fflush_unlocked(file_) != 0) {//刷新文件
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_) != 0 ||
fdatasync(fileno(file_)) != 0) {
s = Status::IOError(filename_, strerror(errno));
}
return s;
}
};
WritableFile类在写入数据时不会对数据进行任何封装、修改操作,而是直接将数据写入到log文件中。
因此我们一般插入一个Key-Value对时,首先会调用batch.Put(key, value);将其组织成一条记录,然后调用Write::AddRecord(),在log文件中找到合适的位置,同时为每一条记录增加一个头部,再将其写入到log文件中。然后调用WritableFile的Flush()、Sync()等方法来对log文件进行操作。