读写操作
在介绍完sstable文件具体的组织方式之后,我们再来介绍一下相关的读写操作。为了便于读者理解,将首先介绍写操作。
写操作
sstable的写操作通常发生在:
- memory db将内容持久化到磁盘文件中时,会创建一个sstable进行写入;
- leveldb后台进行文件compaction时,会将若干个sstable文件的内容重新组织,输出到若干个新的sstable文件中;
对sstable进行写操作的数据结构为tWriter,具体定义如下:
- // tWriter wraps the table writer. It keep track of file descriptor
- // and added key range.
- type tWriter struct {
- t *tOps
- fd storage.FileDesc // 文件描述符
- w storage.Writer // 文件系统writer
- tw *table.Writer
- first, last []byte
- }
主要包括了一个sstable的文件描述符,底层文件系统的writer,该sstable中所有数据项最大最小的key值以及一个内嵌的tableWriter。
一次sstable的写入为一次不断利用迭代器读取需要写入的数据,并不断调用tableWriter的Append
函数,直至所有有效数据读取完毕,为该sstable文件附上元数据的过程。
该迭代器可以是一个内存数据库的迭代器,写入情景对应着上述的第一种情况;
该迭代器也可以是一个sstable文件的迭代器,写入情景对应着上述的第二种情况;
注解
sstable的元数据包括:(1)文件编码(2)大小(3)最大key值(4)最小key值
故,理解tableWriter的Append
函数是理解整个写入过程的关键。
tableWriter
在介绍append函数之前,首先介绍一下tableWriter这个数据结构。主要的定义如下:
- // Writer is a table writer.
- type Writer struct {
- writer io.Writer
- // Options
- blockSize int // 默认是4KiB
- dataBlock blockWriter // data块Writer
- indexBlock blockWriter // indexBlock块Writer
- filterBlock filterWriter // filter块Writer
- pendingBH blockHandle
- offset uint64
- nEntries int // key-value键值对个数
- }
其中blockWriter与filterWriter表示底层的两种不同的writer,blockWriter负责写入data数据的写入,而filterWriter负责写入过滤数据。
pendingBH记录了上一个dataBlock的索引信息,当下一个dataBlock的数据开始写入时,将该索引信息写入indexBlock中。
Append
一次append函数的主要逻辑如下:
- 若本次写入为新dataBlock的第一次写入,则将上一个dataBlock的索引信息写入;
- 将keyvalue数据写入datablock;
- 将过滤信息写入filterBlock;
- 若datablock中的数据超过预定上限,则标志着本次datablock写入结束,将内容刷新到磁盘文件中;
- func (w *Writer) Append(key, value []byte) error {
- w.flushPendingBH(key)
- // Append key/value pair to the data block.
- w.dataBlock.append(key, value)
- // Add key to the filter block.
- w.filterBlock.add(key)
- // Finish the data block if block size target reached.
- if w.dataBlock.bytesLen() >= w.blockSize {
- if err := w.finishBlock(); err != nil {
- w.err = err
- return w.err
- }
- }
- w.nEntries++
- return nil
- }
dataBlock.append
该函数将编码后的kv数据写入到dataBlock对应的buffer中,编码的格式如上文中提到的数据项的格式。此外,在写入的过程中,若该数据项为restart点,则会添加相应的restartpoint信息。
filterBlock.append
该函数将kv数据项的key值加入到过滤信息中,具体可见《Leveldb源码解析 -布隆过滤器》
finishBlock
若一个datablock中的数据超过了固定上限,则需要将相关数据写入到磁盘文件中。
在写入时,需要做以下工作:
- 封装dataBlock,记录restart point的个数;
- 若dataBlock的数据需要进行压缩(例如snappy压缩算法),则对dataBlock中的数据进行压缩;
- 计算checksum;
- 封装dataBlock索引信息(offset,length);
- 将datablock的buffer中的数据写入磁盘文件;
- 利用这段时间里维护的过滤信息生成过滤数据,放入filterBlock对用的buffer中;Close
当迭代器取出所有数据并完成写入后,调用tableWriter的Close函数完成最后的收尾工作:
- 若buffer中仍有未写入的数据,封装成一个datablock写入;
- 将filterBlock的内容写入磁盘文件;
- 将filterBlock的索引信息写入metaIndexBlock中,写入到磁盘文件;
- 写入indexBlock的数据;
- 写入footer数据;至此为止,所有的数据已经被写入到一个sstable中了,由于一个sstable是作为一个memorydb或者Compaction的结果原子性落地的,因此在sstable写入完成之后,将进行更为复杂的leveldb的版本更新,将在接下来的文章中继续介绍。