LevelDB 学习笔记4:读写

LevelDB 学习笔记4:读写

部分内容参考 LevelDB Handbook

写操作

写操作分为两步

  • 先写日志
    • 可以防止宕机时内存数据的丢失
    • 但如果是异步写,日志可能没有落盘,而是在操作系统的缓存里,如果操作系统宕机,就会丢失这些日志
  • 再写到内存数据库中

LevelDB 提供两种写操作

Status Put(const WriteOptions& options, 
		   const Slice& key,const Slice& value);

Status Delete(const WriteOptions& options, const Slice& key);

Delete 会被转换为 value 为空的 Put 操作

  • 插入记录的类型会被标记为删除操作
  • 但真正的删除操作发生在合并的时候

🔑 Slice 是 LevelDB 中定义的一种基础类型

  • 内容是 char 指针加长度
  • 不拥有字符串的所有权
  • 和 string_view 有些像

批量操作

此外, LevelDB 还支持批量操作

Status Write(const WriteOptions& options, WriteBatch* updates);

实际上,Put 和 Delete 都会被转换为 Write 操作

WriteBatch

WriteBatch 内部是一个字符串,结构如下:

其中,表示长度的整数都是变长的,编码的方式为 Varint

  • 1 个字节是一组
    • 用 most significant bit 表示是否已经结束
    • 剩下的 7 位用来存储整数的有效内容

写日志时,会将 Write 操作 WriteBatch 中的内容转换为单条日志记录

  • 保证 Write 操作的原子性
    • 如果没做完,但写了日志,DB 崩溃了,重启时,整个 Batch 的操作都会被重做
    • 如果没写日志,整个 Batch 的操作都会被抛弃

序列号

LevelDB 中每个操作都会被赋予一个序列号

  • 内部维护一个计数器,每次操作 +1
  • 由于 LevelDB 中更新和删除数据都是 append 的方式
    • 因此同一个 key 可能存在多条记录
    • 通过序列号可以区分记录的新旧
  • 快照(snapshot)代表 DB 某个时刻下的状态,它也是通过序列号实现的
    • 一个序列号就代表数据库的一份快照
    • 所有用户主动创建的快照存在一个双向链表里

合并写

当存在多个并发写入的操作时,只允许一个 writer 执行写入,为了提高效率,这个 writer 会将其他 writer 的写入操作合并进来,替他们执行,执行完成后通知他们

合并写的整体流程:

线程会先将自己加入写入队列,如果发现自己是队首就有资格执行写入操作,用写入队列而不是直接加锁来决定哪个线程能写入的好处

  • 支持合并写
  • 保证 FIFO,最先来的线程可以优先写入,使得响应时间更短

线程写入前会先合并队列中其他 writer 的 WriteBatch,直到

  • 合并后的 WriteBatch 达到大小阈值
    • 如果发现写入线程的 WriteBatch 比较小,会适当降低大小阈值,避免小的写入操作被过分推迟
  • 队列中所有 writer 的 WriteBatch 都已经被合并
  • 写入线程不要求同步,但后续的某个 writer 要求同步

留足写入空间

写 memtable 前,会先调用 MakeRoomForWrite() 保证 memtable 里有足够的内存可用

  • 如果 L0 文件数量触发 kL0_SlowdownWritesTrigger 阈值,就延迟 1ms 再写入
    • 一次写入操作只会被延迟一次
    • 延迟会移交部分 CPU 时间给后台线程(如果它和写线程在同一 CPU 上)
  • 如果当前的 memtable 满了
    • 如果上次的 immutable memtable 还没写到磁盘里
      • 等待后台合并任务完成
      • 因为内存里最多同时存在两个 memtable
    • 如果 L0 文件数量触发 config::kL0_StopWritesTrigger 阈值
      • 等待后台合并任务完成
    • 否则,将当前的 memtable 转换为 immutable memtable,并创建新的 memtable
      • 如果后台线程没有在运行,就拉起后台线程

读操作

  • 用 user_key 生成一个 internal_key
    • 如果 Get() 操作指定了快照,就用快照做序列号
    • 否则,用当前最新的序列号
  • 用 internal_key 做查询操作保证了只会读到比 snapshot 更旧的数据
    • LevelDB MVCC 实现的一部分
  • 按顺序读取,查到后就不再接着往下查了
    • memtable
    • immutable memtable
    • 按 L0,L1,L2..... 的顺序读取 sstable
  • 可能会触发 Seek Compaction
posted @ 2022-04-18 23:27  路过的摸鱼侠  阅读(271)  评论(0编辑  收藏  举报