【问题标题】:store list in key value database在键值数据库中存储列表
【发布时间】:2013-09-02 00:47:25
【问题描述】:

我寻找在键值数据库中存储与键关联的列表的最佳方式(如 berkleydbleveldb

例如: 我有用户和用户之间的订单 我想为每个用户存储订单 ID 列表,以便通过范围选择(用于分页)快速访问

如何存储这个结构?

我不想为每个用户以可序列化的格式存储它:

user_1_orders = serialize(1,2,3..)
user_2_orders = serialize(1,2,3..)

因为列表可能很长​​p>

我考虑为每个用户使用单独的 db 文件,并将商店订单 ID 作为其中的键,但这并不能解决范围选择问题。如果我想获取范围为 [5000:5050] 的用户 ID,该怎么办?

我知道redis,但对berkleydbleveldb 等键值实现感兴趣。

【问题讨论】:

    标签: nosql berkeley-db leveldb okvs


    【解决方案1】:

    您可以使用 Redis 将列表存储在 zset(sorted set) 中,如下所示:

    // this line is called whenever a user place an order
    $redis->zadd($user_1_orders, time(), $order_id);
    // list orders of the user
    $redis->zrange($user_1_orders, 0, -1);
    

    Redis 足够快。但是关于Redis你应该知道的一点是,它把所有的数据都存储在内存中,所以如果数据最终超出了物理内存,你必须自己对数据进行分片。

    你也可以使用SSDB(https://github.com/ideawu/ssdb),它是leveldb的包装器,与Redis有类似的API,但大部分数据存储在磁盘中,内存仅用于缓存。这意味着 SSDB 的容量是 Redis 的 100 倍——高达 TB。

    【讨论】:

    • redis db 必须适合内存。这不是leveldb和bsddb的情况
    【解决方案2】:

    您可以在支持扫描的键值存储(如 leveldb)中对此进行建模的一种方法是将订单 ID 添加到每个用户的键中。因此,每个订单的新键都是 userId_orderId。现在要获取特定用户的订单,您可以进行简单的前缀扫描 - scan(userId*)。现在这会使 userId 范围查询变慢,在这种情况下,您可以为 userIds 维护另一个表或使用另一个键约定:Id_userId 用于获取 [5000-5050] 之间的 userIds

    最近我看到 hyperdex 在 leveldb 之上添加数据类型支持:例如:http://hyperdex.org/doc/04.datatypes/#lists,所以你也可以尝试一下。

    【讨论】:

      【解决方案3】:

      在 BerkeleyDB 中,您可以按已排序或未排序的顺序存储每个键的多个值。这将是最自然的解决方案。 LevelDB 没有这样的功能。不过你应该看看LMDB(http://symas.com/mdb/),它还支持排序的多值键,并且比其他任何一个都更小、更快、更可靠。

      【讨论】:

      【解决方案4】:

      让我们从一个列表开始。您可以使用单个哈希图:

      1. 在行中存储0用户的订单计数
      2. 为每个新订单存储一个新行并增加计数

      所以 yoru hashmap 如下所示:

      key | value
      -------------
       0  |   5
       1  | tomato
       2  | celery
       3  | apple
       4  | pie
       5  | meat
      

      密钥的稳定递增确保每个密钥都是唯一的。鉴于 db 是键排序的,并且 pack 函数将整数转换为一组正确排序的字节数组,您可以获取列表的切片。要获取 5000 到 5050 之间的订单,您可以使用 bsddb Cursor.set_range 或 leveldb 的 createReadStream (js api)

      现在让我们扩展到多个用户订单。如果你可以打开几个hashmap你可以使用上面使用的几个hashmap。也许您会遇到一些系统问题(打开 fds 的最大 nb 或每个目录的最大文件数)。因此,您可以使用单个并为多个用户共享相同的 hashmap。

      我在下面解释的内容适用于 leveldb 和 bsddb,因为您 pack 正确使用字典顺序(字节顺序)键。所以我假设你有一个pack 函数。在 bsddb 中,您必须自己构建一个 pack 函数。看看wiredtiger.packingbytekey 以获得灵感。

      原理是使用用户的 id 来命名键。它也称为键组合。

      假设您的数据库如下所示:

         key   |  value
      -------------------
        1  | 0 |    2       <--- count column for user 1
        1  | 1 |  tomato
        1  | 2 |  orange 
          ...      ...
        32 | 0 |    1       <--- count column for user 32
        32 | 1 |  banna
          ...  |   ...
      

      您使用以下(伪)代码创建此数据库:

      db.put(pack(1, make_uid(1)), 'tomato')
      db.put(pack(1, make_uid(1)), 'orange')
      ...
      db.put(pack(32, make_uid(32)), 'bannana')
      

      make_uid 的实现如下所示:

      def make_uid(user_uid):
          # retrieve the current count
          counter_key = pack(user_uid, 0)
          value = db.get(counter_key)
          value += 1  # increment
          # save new count
          db.put(counter_key, value)
          return value
      

      然后您必须进行正确的范围查找,它类似于单个复合键。使用 bsddb api cursor.set_range(key) 我们检索所有项目 对于用户42,在50005050 之间:

      def user_orders_slice(user_id, start, end):
          key, value = cursor.set_range(pack(user_id, start))
          while True:
              user_id, order_id = unpack(key)
              if order_id > end:
                  break
              else:
                  # the value is probably packed somehow...
                  yield value
                  key, value = cursor.next()
      

      未进行错误检查。除此之外,如果您从列表中删除项目,则切片 user_orders_slice(42, 5000, 5050) 不能保证撕掉 51 个项目。查询 50 项目的正确方法是实现 user_orders_query(user_id, start, limit)`。

      我希望你能明白。

      【讨论】:

      • 这让我很容易掌握这个概念。非常感谢。
      猜你喜欢
      • 2012-03-09
      • 2016-08-20
      • 2016-11-27
      • 1970-01-01
      • 1970-01-01
      • 2013-01-31
      • 1970-01-01
      • 2014-11-27
      • 2013-08-14
      相关资源
      最近更新 更多