leveldb之Put、Get操作

本文详细介绍了leveldb的Put和Get操作。在Put操作中,记录通过Put方法写入,包括kTypeValue和kTypeDeletion类型,数据先写入log文件,再进入Memtable。如果Memtable满,则转为immutable并后台转化为SSTable。Get操作则按照Memtable、immutable memtable和SSTable的顺序查找,利用version管理和table_cache加速查找过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一个简单的leveldb使用示例如下:

#include <assert.h>
#include <iostream>
#include "leveldb/db.h"
#include "leveldb/env.h"
using namespace std;

#include <assert.h>
#include <iostream>
#include "leveldb/db.h"
#include "leveldb/env.h"

using namespace std;

int main()
{
leveldb::DB *db;
leveldb::Options ops;
ops.create_if_missing=true;
std::string dbpath="testdb";
leveldb::Status status=leveldb::DB::Open(ops,dbpath,&db);
assert(status.ok());
string key="lili";
string value="hihi";

string res;
status=db->Put(leveldb::WriteOptions(),key,value);//将Key-Value插入到leveldb中
status=db->Get(leveldb::ReadOptions(),key,&res);//根据Key值在leveldb中查找其对应的Value,返回值存放在res中

cout<<res<<endl;
delete db;
return 0;
}

Put操作

1、根据leveldb的源码可知,leveldb::DB是一个抽象基类,其中定义了一些纯虚函数,包括Put和Get等,使其只能作为父类被继承,而不能被实例化。leveldb::DBImpl继承自该基类,因此在调用db->Put()时,首先调用的是leveldb::DBImpl->Put()方法:

Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val) {
  return DB::Put(o, key, val);
}

在该方法中,只是简单的调用父类leveldb::DB实现的Put()方法,leveldb::DBImpl只是提供一个接口。

2、以前以为纯虚函数是只能定义不能实现的,通过leveldb才发现原来纯虚函数也是可以实现的,然后被显式调用。leveldb::DB中的Put方法如下:

Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
  WriteBatch batch;
  batch.Put(key, value);//将key,value组织成一条记录存放在batch中
  return Write(opt, &batch);//调用Write方法写入记录
}

一条记录包含如下内容:
Type、Key、Value
当要插入记录时,Type为kTypeValue,当要删除记录时,Type为kTypeDeletion,同时中每一个batch都有一个对当前批处理记录信息的统计(sequence(8字节)和count(4字节),共12字节)
由此可见,当我们要删除一个数据时,并不是直接从内存中删除,而是插入一条带有删除标志的记录

在本例中要插入数据:key=”lili”; value=”hihi”;
由之前对WriterBatch的分析可知,得到的batch为:
01 00 00 00 00 00 00 00 01 00 00 00 (前8字节表示是第一个batch,后4字节表示此batch中只有一条记录)
01(kTypeValue) 04(Key.size) 6C 69 6C 69(lili) 04(value.size) 68 69 68 69(hihi)
共12+1+1+4+1+4=23字节=0x17

3、然后调用leveldb::DBImpl->Write()写入记录:

Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) {
  Writer w(&mutex_);
  w.batch = my_batch;
  w.sync = options.sync;//default is false
  w.done = false;

  MutexLock l(&mutex_);
  writers_.push_back(&w);//将writer加入任务队列deque
  while (!w.done && &w != writers_.front()) {//未执行,且不在任务队列首部,则等待
    w.cv.Wait();
  }
  if (w.done) {//已执行完毕,返回status
    return w.status;
  }

  //确保有Memtable和log文件可以用于数据的写入,对已写满的Memtable后台调度Compaction
  Status status = MakeRoomForWrite(my_batch == NULL);
  uint64_t last_sequence = versions_->LastSequence();
  Writer* last_writer = &w;
  if (status.ok() && my_batch != NULL) {// 当batch为空时,是准备执行compactions操作,否则插入记录
    WriteBatch* updates = BuildBatchGroup(&last_writer);//将任务队列中所有的非同步任务组织在一起形成一个WriteBatch,一起批量写入,可以极大的提升写的效率
    WriteBatchInternal::SetSequence(updates, last_sequence + 1);
    last_sequence += WriteBatchInternal::Count(updates);

    {
      mutex_.Unlock();
      status = log_->AddRecord(WriteBatchInternal::Contents(updates));//调用fwrite将记录写入log文件中
      bool sync_error = false;
      if (status.ok() && options.sync) {
        status = logfile_->Sync();//若设置了同步,则每次写成功后都同步一次
      }
      if (status.ok()) {
        status = WriteBatchInternal::InsertInto(updates, mem_);//将updates中的记录插入到mem_中
      }
      mutex_.Lock();
    }
    if (updates == tmp_batch_) tmp_batch_->Clear();
    versions_->SetLastSequence(last_sequence);
  }

  while (true) {//等待队列中的其它任务
    Writer* ready = writers_.front();
    writers_.pop_front();
    if (ready != &w) {
      ready->status = status;
      ready->done = true;
      ready->cv.Signal();
    }
    if (ready == last_writer) break;
  }

  // Notify new head of write queue
  if (!writers_.empty()) {
    writers_.front()->cv.Signal();
  }
  return status;
}

通过调用log_->AddRecord()将记录写入到log中,由之前对log文件写操作的分析 可知,log文件每次写入一个batch的时候都会在前面为其加上7字节的首部(CRC(4字节)、记录长度(2字节)、type(1字节)),其中CRC与type有关。
对于本例,length=23=0x17字节
记录为第一个,且不会占满当前block,因此type为 kFullType=0x01

最终在testdb目录下的.log文件中可看到如下结果:
00000000h: 66 5F 61 EE 17 00 01 01 00 00 00 00 00 00 00 01 ; f_a?………..
00000010h: 00 00 00 01 04 6C 69 6C 69 04 68 69 68 69 ; …..lili.hihi

向log文件中写入成功后,会调用WriteBatchInternal::InsertInto()将记录插入到Memtable中,记录在Memtable中是按照user_key升序,sequence降序排列的,这样所有user _key相同的记录都是聚集在一起的,且其中第一个就是最新的记录,在后面的合并操作中,我们只需要处理相同user _key的第一条记录即可,后面的都可以丢弃。
写操作可能会导致Memtable写满,此时就需要将其转化为immutable memtable,并在后台调用合并操作将其转化为SSTable。这是在MakeRoomForWrite()中完成的。

Get操作

db->Get()会调用DBImpl::Get(const ReadOptions& options,const Slice& key,std::string* value),查找key对应的值存放到value中。

Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value) {
  Status s;
  MutexLock l(&mutex_);
  MemTable* mem = mem_;
  MemTable* imm = imm_;
  Version* current = versions_->current();
  mem->Ref();
  if (imm != NULL) imm->Ref();
  current->Ref();

  bool have_stat_update = false;
  Version::GetStats stats;

  // Unlock while reading from files and memtables
  {//主要的查找操作,,
    mutex_.Unlock();
    // First look in the memtable, then in the immutable memtable (if any).
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &s)) {//首先在memtable中查找
      // Done
    } else if (imm != NULL && imm->Get(lkey, value, &s)) {//若没找到则继续在immutable memtable中查找
      // Done
    } else {//若还没找到,则继续在sstable中查找
      s = current->Get(options, lkey, value, &stats);
      have_stat_update = true;
    }
    mutex_.Lock();
  }

  if (have_stat_update && current->UpdateStats(stats)) {
    MaybeScheduleCompaction();//可能进行campact操作
  }
  mem->Unref();
  if (imm != NULL) imm->Unref();
  current->Unref();
  return s;
}

由上可知,leveldb是按照Memtable、immutable memtable,SSTable的优先级来进行查找的。其中每个SSTable文件都是有查找次数限制的,在FileMetaData(记录每个.sst文件信息的数据结构)中被初始化的。因此在SSTable中查找时,也可能会触发合并操作。
在查找时用到了version,leveldb 使用 version 来保存数据库的状态,Version 保存了所有level的所有的SSTable文件信息,通过version->Current()可用来获取”current” version(当前版本)。
在SSTable中查找时,首先是从低到高遍历当前Version中所有level中的所有文件,逐个找出Key值范围覆盖了目标Key值的文件,然后依次在这些文件中进行查找。
每一个数据库中都有一个缓存变量table_cache,用于缓存最近使用的.sst文件信息,由于内存访问速度比磁盘访问速度快得多,这样可以极大的提高查找的效率。因此在查找.sst文件时,若.sst文件不在缓存中,则将其加入缓存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值