读写操作

在介绍完sstable文件具体的组织方式之后,我们再来介绍一下相关的读写操作。为了便于读者理解,将首先介绍写操作。

写操作

sstable的写操作通常发生在:

  • memory db将内容持久化到磁盘文件中时,会创建一个sstable进行写入;
  • leveldb后台进行文件compaction时,会将若干个sstable文件的内容重新组织,输出到若干个新的sstable文件中;

对sstable进行写操作的数据结构为tWriter,具体定义如下:

  1. // tWriter wraps the table writer. It keep track of file descriptor
  2. // and added key range.
  3. type tWriter struct {
  4. t *tOps
  5.  
  6. fd storage.FileDesc // 文件描述符
  7. w storage.Writer // 文件系统writer
  8. tw *table.Writer
  9.  
  10. first, last []byte
  11. }

主要包括了一个sstable的文件描述符,底层文件系统的writer,该sstable中所有数据项最大最小的key值以及一个内嵌的tableWriter。

一次sstable的写入为一次不断利用迭代器读取需要写入的数据,并不断调用tableWriter的Append函数,直至所有有效数据读取完毕,为该sstable文件附上元数据的过程。

该迭代器可以是一个内存数据库的迭代器,写入情景对应着上述的第一种情况;

该迭代器也可以是一个sstable文件的迭代器,写入情景对应着上述的第二种情况;

注解

sstable的元数据包括:(1)文件编码(2)大小(3)最大key值(4)最小key值

故,理解tableWriter的Append函数是理解整个写入过程的关键。

tableWriter

在介绍append函数之前,首先介绍一下tableWriter这个数据结构。主要的定义如下:

  1. // Writer is a table writer.
  2. type Writer struct {
  3. writer io.Writer
  4. // Options
  5. blockSize int // 默认是4KiB
  6.  
  7. dataBlock blockWriter // data块Writer
  8. indexBlock blockWriter // indexBlock块Writer
  9. filterBlock filterWriter // filter块Writer
  10. pendingBH blockHandle
  11. offset uint64
  12. nEntries int // key-value键值对个数
  13. }

其中blockWriter与filterWriter表示底层的两种不同的writer,blockWriter负责写入data数据的写入,而filterWriter负责写入过滤数据。

pendingBH记录了上一个dataBlock的索引信息,当下一个dataBlock的数据开始写入时,将该索引信息写入indexBlock中。

Append

一次append函数的主要逻辑如下:

  • 若本次写入为新dataBlock的第一次写入,则将上一个dataBlock的索引信息写入;
  • 将keyvalue数据写入datablock;
  • 将过滤信息写入filterBlock;
  • 若datablock中的数据超过预定上限,则标志着本次datablock写入结束,将内容刷新到磁盘文件中;
  1. func (w *Writer) Append(key, value []byte) error {
  2. w.flushPendingBH(key)
  3. // Append key/value pair to the data block.
  4. w.dataBlock.append(key, value)
  5. // Add key to the filter block.
  6. w.filterBlock.add(key)
  7.  
  8. // Finish the data block if block size target reached.
  9. if w.dataBlock.bytesLen() >= w.blockSize {
  10. if err := w.finishBlock(); err != nil {
  11. w.err = err
  12. return w.err
  13. }
  14. }
  15. w.nEntries++
  16. return nil
  17. }

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的版本更新,将在接下来的文章中继续介绍。