Redis有序集合及存储UID的最佳方式

5
我有一组由用户ID和这些用户ID的标签组成的数据。 用户ID多次出现,而标签数量预先设定为500,但未来可能会更改。必须存储的是用户ID、他们的标签和计数。 我想要轻松地找到得分最高的标签等等。每次出现标签都会增加

我的redis实现使用排序集完成
  • 每个user_id是一个排序集

  • 键是user_id,是十六进制数字

操作方式如下:

zincrby user_id:x 1 "tag0"

zincrby user_id:x 1 "tag499"

zincrby user_id:y 1 "tag3"

以此类推

考虑到我想要得到得分最高的标签,是否有更好的方法?

第二个问题是,我现在正在使用"keys *"来检索这些键进行客户端操作,但我知道这不适用于生产系统。

对于内存问题,遍历指定数量的键(在10000范围内)会更好。我知道键必须存储在内存中,但它们没有遵循特定模式,以允许部分检索,从而避免"zmalloc"错误(4GB 64位debian服务器)。 键的数量约为2000万。有什么想法吗?

1个回答

14
我的第一点是要注意,4GB的空间对于存储20M个已排序集合来说是紧张的。一个快速尝试表明,在64位系统上,20M个用户,每个用户带有20个标签,大约需要8GB的空间(这还计算了Redis 2.4提供的已排序集合ziplist内存优化 - 不要在早期版本中尝试这样做)。
已排序集合是支持您的用例的理想数据结构。我会按照您描述的方式使用它们。
正如您指出的那样,KEYS命令不能用于迭代键。它更像是一个调试命令。为了支持键迭代,您需要添加一个数据结构来提供此访问路径。Redis中唯一支持迭代的结构是列表和已排序集合(通过范围方法)。但是,它们往往会将O(n)迭代算法转换为O(n^2)(列表),或者O(nlogn)(有序集合)。列表也不适合存储键,因为当键被添加/删除时,维护它将变得困难。
更有效的解决方案是添加一个由常规集合组成的索引。您需要使用哈希函数将特定用户与桶关联起来,并将用户ID添加到相应的集合中。如果用户ID是数字值,则简单的模函数就足够了。如果它们不是,一个简单的字符串哈希函数就可以解决问题。
因此,为了支持对用户1000、用户2000和用户1001进行迭代,让我们选择一个模1000函数。将用户1000和用户2000放在桶索引:0中,而将用户1001放在桶索引:1中。
因此,在已排序集合之上,我们现在有以下键:
index:0 => set[ 1000, 2000 ]
index:1 => set[ 1001 ]
在集合中,键的前缀是不必要的,这使得Redis可以通过序列化集合来优化内存消耗,只要它们被保持足够小(由Sripathi Krishnan提出的整数集优化)。
全局迭代包括对从0到1000(不包括1000)的桶的简单循环。对于每个桶,应用SMEMBERS命令以检索相应的集合,然后客户端就可以迭代单个项。
以下是Python示例:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ----------------------------------------------------

import redis, random

POOL = redis.ConnectionPool(host='localhost', port=6379, db=0)

NUSERS = 10000
NTAGS = 500
NBUCKETS = 1000

# ----------------------------------------------------
# Fill redis with some random data

def fill(r):
  p = r.pipeline()
  # Create only 10000 users for this example
  for id in range(0,NUSERS):
    user = "user:%d" % id
    # Add the user in the index: a simple modulo is used to hash the user id
    # and put it in the correct bucket
    p.sadd( "index:%d" % (id%NBUCKETS), id )
    # Add random tags to the user
    for x in range(0,20):
      tag = "tag:%d" % (random.randint(0,NTAGS))
      p.zincrby( user, tag, 1 )
    # Flush the pipeline every 1000 users
    if id % 1000 == 0:
      p.execute()
      print id
  # Flush one last time
  p.execute()

# ----------------------------------------------------
# Iterate on all the users and display their 5 highest ranked tags

def iterate(r):
  # Iterate on the buckets of the key index
  # The range depends on the function used to hash the user id
  for x in range(0,NBUCKETS):
    # Iterate on the users in this bucket
    for id in r.smembers( "index:%d"%(x) ):
      user = "user:%d" % int(id)
      print user,r.zrevrangebyscore(user,"+inf","-inf", 0, 5, True )

# ----------------------------------------------------
# Main function

def main():
  r = redis.Redis(connection_pool=POOL)
  r.flushall()
  m = r.info()["used_memory"]
  fill(r)
  info = r.info()
  print "Keys: ",info["db0"]["keys"]
  print "Memory: ",info["used_memory"]-m
  iterate(r)

# ----------------------------------------------------

main()

通过调整常量,您还可以使用此程序来评估此数据结构的全局内存消耗。

在我看来,这种策略简单而高效,因为它提供了O(1)复杂度以添加/删除用户,并且真正的O(n)复杂度以迭代所有项。唯一的缺点是密钥迭代顺序是随机的。


2
我会将 index:0 => set[user:1000, user:1001,... user:2000 ] 改为 index:0 => set[ 1000, 1001, ... 2000 ],以利用整数集合的优势,但除此之外,这是一个很好的策略! - Sripathi Krishnan
@Didier 感谢您的分享。这仍然是您首选的迭代方法吗?我看到自从您写这篇文章以来,已经实现了SCAN。 - chishaku
现在,使用SCAN迭代是一种更简单的方式。 - Didier Spezia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接