源码:
log_writer.cc。
为了不丢失数据,需要持久化的功能。把数据持久化到磁盘上,最常用的技术就是日志技术,也就是当修改数据时,先把对数据的修改写到磁盘上,然后在MemTable里做修改。因为日志记录了每个操作,只要对日志进行重放便可以恢复MemTable,这样就做到了数据库实例崩溃、宕机或者停机维护的时候数据不丢失。
这种日志技术在数据库里面很常用(Redis里的Aof,Innodb里的Redo Log都是这样的技术),一般称为WAL (Write Ahead Log),正如名字一样,就是在写入前先写入日志的意思。
Put操作也不能简单的写入到 memtable,而是要先写日志、再写 memtable,都更新完成后再返回写入成功,write-ahead logging 名字也是由此而来。
因为日志的写入都是Append
的,也就是顺序写,所以写磁盘也是顺序写,虽然磁盘的随机写效率比较低,但是顺序写效率还是很高的,所以就算加入了日志,写效率还是很高,依然可以满足写多的场景。
另外可以通过控制日志写同步的策略在性能和可靠性之间做折中:
- 每次写入都做一次
sync
,可靠性最高,不会丢数据,但是性能最低; - 每次写入,但是不
sync
,数据库崩溃不会丢数据,但是机器崩溃会丢数据,性能高; - 每次写入,不
sync
,但是每1s做一次sync
,数据库崩溃不会丢数据,但是机器崩溃丢最多1s的数据,性能较高。
有了MemTable和WAL,就有了一个功能比较完备的数据库了,类似于一个简化版的Redis。然而这又引入了新的问题:
- 如果日志量很大,每次重启数据库时,恢复的时间非常长;
- 内存的容量是有限的,所以当数据量太大,MemTable的大小超过内存容量时,就没法写入数据了。
这时候不仅仅需要将日志写入到磁盘,也需要定时将MemTable的镜像写入磁盘,并且清空MemTable。
leveldb 里的日志写操作主要由leveldb::log::Writer完成。log::Writer类主要负责:接收待写入数据,组织数据格式,调用成员变量完成数据真正写入到文件系统。
日志的格式
例如当前 block 只有10个字节,而用户调用了AddRecord('HelloWorld'),数据会分为两次写入,type 分别为 kFirstType kLastType:
************** block N **************
...
|crc 3 kFirstType|hel|
************** block N+1 **************
|crc 7 kLastType|loWorld|
...
可以看到先尝试写入hel填满该 block,标记这次写入为kFirstType,总共占用 10 个字节。然后写入loWorld到下一个 block,标记为kLastType.
如果这个 block 写不下,那么就标记为kMiddleType.
type 是一个枚举值:
enum RecordType {
// Zero is reserved for preallocated files
kZeroType = 0,
kFullType = 1,
// For fragments
kFirstType = 2,
kMiddleType = 3,
kLastType = 4
};
如果 block 剩余的空间不足7 bytes,写不下 header,那么就补\0填满。
简而言之:一个block只有32k,来一条数据时,能放得下就放,放不下就能放多少算多少。剩下的放入下个blck,其中用type来标记是第几块。
在 level 中,db_impl.cc会构造log::Writer对象并且写入数据。
WriteBatchInternal::Contents(updates)数据格式图里的 data 部分,这些究竟包含了什么?可以参考写入与读取流程这篇笔记。
//WriterBatch写入log文件,包括:sequence,操作count,每次操作的类型(Put/Delete),key/value及其长度
status = log_->AddRecord(WriteBatchInternal::Contents(updates));
bool sync_error = false;
if (status.ok() && options.sync) {
//log_底层使用logfile_与文件系统交互,调用Sync完成写入
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
//写入文件系统后不用担心数据丢失,继续插入MemTable
if (status.ok()) {
status = WriteBatchInternal::InsertInto(updates, mem_);
}