rocksdb的读写合并操作add

采用rocksdb的合并操作是为了区分get,set的操作,对于合并操作,更大的意义在于修改,如果使用set也可以实现对应的功能,但是需要去库中查询,get获取到当前的value,在进行外部的合并,在进行set操作,过于冗余,所以提出合并的操作。

为什么

RocksDB 是一种高性能的嵌入式持久键值存储。传统上,它提供三个简单的操作Get,Put和Delete,以允许一个优雅的类似查找表的界面。https://github.com/facebook/rocksdb/blob/main/include/rocksdb/db.h
通常,以某种方式更新现有值是一种常见模式。要在rocksdb中执行此操作,客户端必须读取(获取)现有值,对其进行修改,然后将其写回(放置)数据库。让我们看一个具体的例子。
假设我们正在维护一组 uint64 计数器。每个计数器都有不同的名称。我们希望支持四个高级操作:设置、添加、获取和删除。
首先,我们定义接口并获得正确的语义。为清楚起见,错误处理被忽略了。

class Counters {
 public:
  // (re)set the value of a named counter
  virtual void Set(const string& key, uint64_t value);

  // remove the named counter
  virtual void Remove(const string& key);

  // retrieve the current value of the named counter, return false if not found
  virtual bool Get(const string& key, uint64_t *value);

  // increase the named counter by value.
  // if the counter does not exist,  treat it as if the counter was initialized to zero
  virtual void Add(const string& key, uint64_t value);
  };

其次,我们使用现有的 rocksdb 支持来实现它。伪代码如下:

    class RocksCounters : public Counters {
     public:
      static uint64_t kDefaultCount = 0;
      RocksCounters(std::shared_ptr<DB> db);

      // mapped to a RocksDB Put
      virtual void Set(const string& key, uint64_t value) {
        string serialized = Serialize(value);
        db_->Put(put_option_, key,  serialized));
      }

      // mapped to a RocksDB Delete
      virtual void Remove(const string& key) {
        db_->Delete(delete_option_, key);
      }

      // mapped to a RocksDB Get
      virtual bool Get(const string& key, uint64_t *value) {
        string str;
        auto s = db_->Get(get_option_, key,  &str);
        if (s.ok()) {
          *value = Deserialize(str);
          return true;
        } else {
          return false;
        }
      }

      // implemented as get -> modify -> set
      virtual void Add(const string& key, uint64_t value) {
        uint64_t base;
        if (!Get(key, &base)) {
          base = kDefaultCount;
        }
        Set(key, base + value);
      }
    };

请注意,除了 Add 操作之外,所有其他三个操作都可以直接映射到 rocksdb 中的单个操作。编码方面,还不错。但是,概念上的单个操作 Add 仍然映射到两个 rocksdb 操作。这也对性能有影响 - 随机获取在rocksdb中相对较慢。

现在,假设我们要将计数器作为服务托管。考虑到当今服务器的核心数量,我们的服务几乎可以肯定是多线程的。如果线程未按键空间分区,则同一计数器的多个 Add 请求可能会被不同的线程选取并同时执行。好吧,如果我们也有严格的一致性要求(缺少更新是不可接受的),我们将不得不用外部同步包装 Add,某种锁。开销加起来。

如果 RocksDb 直接支持 Add 功能会怎样?那么我们可能会想出这样的东西:

    virtual void Add(const string& key, uint64_t value) {
      string serialized = Serialize(value);
      db->Add(add_option, key, serialized);
    }

这对于计数器来说似乎是合理的。但并不是你存储在RocksDB中的所有东西都是计数器。假设我们需要跟踪用户去过的位置。我们可以存储一个(序列化的)位置列表作为用户密钥的值。将新位置添加到现有列表将是一种常见操作。在这种情况下,我们可能需要一个追加操作:db->Append(user_key, serialize(new_location))。这表明读取-修改-写入操作的语义实际上是客户端值类型确定的。为了保持库的通用性,我们最好抽象出此操作,并允许客户端指定语义。这就引出了我们的建议:合并。

什么

我们在 RocksDB 中开发了一个通用的合并操作作为新的一等操作来捕获读取-修改-写入语义。
此合并操作:
将读取-修改-写入的语义封装到一个简单的抽象接口中。
允许用户避免因重复 Get() 调用而产生额外费用。
执行后端优化,以决定何时/如何在不更改基础语义的情况下组合操作数。
在某些情况下,可以摊销所有增量更新的成本,以提供渐近的效率提高。

如何使用它

在以下部分中,将介绍特定于客户端的代码更改。我们还简要概述了如何使用合并。 假设读者已经知道如何使用经典的RocksDB(或LevelDB),包括:
DB 类(包括构造、DB:😛 ut()、DB::Get() 和 DB:😄 elete())
选项类(以及如何在创建时指定数据库选项)
知道写入数据库的所有键/值都是简单的字节字符串。

接口概述

我们定义了一个新的接口/抽象基类:MergeOperator。 它公开了一些函数,告诉 RocksDB 如何将增量更新操作(称为“合并操作数”)与基值(放置/删除)相结合。 这些函数还可用于告诉 RocksDB 如何将合并操作数相互组合以形成新的合并操作数(称为“部分”或“关联”合并)。
为简单起见,我们将暂时忽略部分合并与非部分合并的概念。 因此,我们提供了一个名为AssociaciativeMergeOperator的单独接口,该接口封装并隐藏了有关部分合并的所有详细信息。 而且,对于大多数简单的应用程序(例如上面的 64 位计数器示例),这就足够了。
因此,读者应该假设所有合并都是通过一个名为AssociaciativeMergeOperator的接口处理的。 这是公共接口:

    // The Associative Merge Operator interface.
    // Client needs to provide an object implementing this interface.
    // Essentially, this class specifies the SEMANTICS of a merge, which only
    // client knows. It could be numeric addition, list append, string
    // concatenation, ... , anything.
    // The library, on the other hand, is concerned with the exercise of this
    // interface, at the right time (during get, iteration, compaction...)
    class AssociativeMergeOperator : public MergeOperator {
     public:
      virtual ~AssociativeMergeOperator() {}

      // Gives the client a way to express the read -> modify -> write semantics
      // key:           (IN) The key that's associated with this merge operation.
      // existing_value:(IN) null indicates the key does not exist before this op
      // value:         (IN) the value to update/merge the existing_value with
      // new_value:    (OUT) Client is responsible for filling the merge result here
      // logger:        (IN) Client could use this to log errors during merge.
      //
      // Return true on success. Return false failure / error / corruption.
      virtual bool Merge(const Slice& key,
                         const Slice* existing_value,
                         const Slice& value,
                         std::string* new_value,
                         Logger* logger) const = 0;

      // The name of the MergeOperator. Used to check for MergeOperator
      // mismatches (i.e., a DB created with one MergeOperator is
      // accessed using a different MergeOperator)
      virtual const char* Name() const = 0;

     private:
      ...
    };

一些注意事项:

AssociaciativeMergeOperator是一个名为MergeOperator的类的子类。稍后我们将看到,在某些情况下,更通用的 MergeOperator 类可能更强大。另一方面,我们在这里使用的AssociaciativeMergeOperator是一个更简单的界面。
existing_value可能是空的。这在合并操作是键的第一个操作的情况下很有用。nullptr 表示“现有”值不存在。这基本上是听从客户端在没有预值的情况下解释合并操作的语义。客户可以做任何合理的事情。例如,计数器::添加假定零值(如果不存在)。
我们传入密钥,以便客户端可以基于它多路复用合并运算符,如果密钥空间被分区并且不同的子空间引用具有不同合并操作语义的不同类型的数据。例如,客户端可以选择将用户帐户的当前余额(数字)存储在键“BAL:uid”下,将帐户活动的历史记录(列表)存储在键“HIS:uid”下,放在同一个数据库中。(这是否是一种好的做法是有争议的)。对于当前余额,数字加法是一个完美的合并运算符;对于活动历史记录,我们需要附加一个列表。因此,通过将密钥传递回 Merge 回调,我们允许客户端区分这两种类型。
例:

 void Merge(...) {
   if (key start with "BAL:") {
     NumericAddition(...)
   } else if (key start with "HIS:") {
     ListAppend(...);
   }
 }

获取合并操作数

这是一个 API,用于获取与键关联的所有合并操作数。此 API 的主要动机是支持不需要执行完全联机合并的用例,因为它对性能敏感。此 API 从版本 6.4.0 开始提供。
示例用例:

存储 KV 对,其中 V 是排序整数和新值的集合可能会附加到集合中,随后用户希望在集合中搜索值。
示例 KV:键:“某键”值:
[2]、[3,4,5]、[21,100]、[1,6,8,9]
要存储这样的 KV 对,用户通常会将合并 API 调用为:
a.db→Merge(WriteOptions(), ‘some-Key’, ‘2’);
b.db→Merge(WriteOptions(), ‘Some-Key’, ‘3,4,5’);
c.db→Merge(WriteOptions(), ‘Some-Key’, ‘21,100’);
d.db→Merge(WriteOptions(), ‘Some-Key’, ‘1,6,8,9’);
并实现一个合并运算符,该运算符只需在获取 API 调用时将值转换为 [2,3,4,5,21,100,1,6,8,9],然后搜索结果值。在这种情况下,在线合并是不必要的,只需返回所有操作数 [2]、[3,4,5]、[21, 100] 和 [1,6,8,9],然后搜索子列表被证明更快,同时节省 CPU 并实现相同的结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值