[leveldb] 7-Get操作

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/simonyucsdy/article/details/81608867

leveldb虽然支持多线程,但本质上并没有使用一些复杂的数据结构来达成无锁多写多读,而是坚持自然朴实的有锁单写多读。那么是不是只有对时间线产生变动的操作(Put, Compaction etc.)才需要上锁? 不是的。所有操作几乎都要在某一时间上锁来确保结果是线性的符合预期的。怎么讲? 用户在t1建立了快照,那就一定不能得到t2时才写入的数据。在t1建立快照这件事对数据库来说没有改变时间线(没有副作用, 不需要锁),但为了让快照成功建立,那就要上锁,不能有两个线程同时建立快照。所以多线程在很多情况下就是个伪命题,反正最后也会用各种锁模拟出顺序时间线,那还不如event loop呢。

Code:db/db_impl.cc(1117-1138)

Status DBImpl::Get(const ReadOptions& options,
                   const Slice& key,
                   std::string* value) {
  Status s;
  MutexLock l(&mutex_);
  SequenceNumber snapshot;
  if (options.snapshot != nullptr) {
    snapshot =
        static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();
  } else {
    snapshot = versions_->LastSequence();
  }

  MemTable* mem = mem_;
  MemTable* imm = imm_;
  Version* current = versions_->current();
  mem->Ref();
  if (imm != nullptr) imm->Ref();
  current->Ref();

  bool have_stat_update = false;
  Version::GetStats stats;

以上代码在锁的保护下完成了两件事,

  1. 生成一个SequenceNumber作为标记, 后续不管线程会不会被切出去, 结果都要相当于在这个时间点瞬间完成
  2. memtable, immemtable, Version, 由于采用了引用计数, 这里Ref()一下

快照建立完了, 接下来的操作只会有单纯的读, 可以把锁暂时释放

Code:db/db_impl.cc(1140-1154)

 // 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)) {
      // Done
    } else if (imm != nullptr && imm->Get(lkey, value, &s)) {
      // Done
    } else {
      s = current->Get(options, lkey, value, &stats);
      have_stat_update = true;
    }
    mutex_.Lock();
  }

查询先找memtable, 再immemtable, 最后是SSTable, 这都很正常.

请注意我标注了黑科技那行的"LookupKey", 工程师用了些特别的技巧. 这个类主要的功能是把输入的key转换成用于查询的key. 比如key是"Sherry", 实际在数据库中的表达可能会是"6Sherry", 6是长度. 这样比对key是否相等时速度会更快.

Code:db/dbformat.cc(121-138)

LookupKey::LookupKey(const Slice& user_key, SequenceNumber s) {
  size_t usize = user_key.size();
  size_t needed = usize + 13;  // A conservative estimate
  char* dst;
  if (needed <= sizeof(space_)) {
    dst = space_;
  } else {
    dst = new char[needed];
  }
  start_ = dst;
  dst = EncodeVarint32(dst, usize + 8);
  kstart_ = dst;
  memcpy(dst, user_key.data(), usize);
  dst += usize;
  EncodeFixed64(dst, PackSequenceAndType(s, kValueTypeForSeek));
  dst += 8;
  end_ = dst;
}

LookupKey格式 = 长度 + key + SequenceNumber + type

tricks:

  1. 在栈上分配一个200长度的数组, 如果运行时发现长度不够用再从堆上new一个, 可以极大避免内存分配
  2. 黑科技函数"EncodeVarint32", 一般key的长度不可能用满32bit. 大量很短的Key却要用32bit来描述长度无疑是很浪费的. 这个函数让小数值用更少的空间, 代价是最糟要多花一字节(8bit)

 Code:db/dbformat.cc(47-73)

char* EncodeVarint32(char* dst, uint32_t v) {
  // Operate on characters as unsigneds
  unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
  static const int B = 128;
  if (v < (1<<7)) {
    *(ptr++) = v;
  } else if (v < (1<<14)) {
    *(ptr++) = v | B;
    *(ptr++) = v>>7;
  } else if (v < (1<<21)) {
    *(ptr++) = v | B;
    *(ptr++) = (v>>7) | B;
    *(ptr++) = v>>14;
  } else if (v < (1<<28)) {
    *(ptr++) = v | B;
    *(ptr++) = (v>>7) | B;
    *(ptr++) = (v>>14) | B;
    *(ptr++) = v>>21;
  } else { // 最多用5字节
    *(ptr++) = v | B;
    *(ptr++) = (v>>7) | B;
    *(ptr++) = (v>>14) | B;
    *(ptr++) = (v>>21) | B;
    *(ptr++) = v>>28;
  }
  return reinterpret_cast<char*>(ptr);
}

猜你喜欢

转载自blog.csdn.net/simonyucsdy/article/details/81608867