我的第一点是要注意,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示例:
import redis, random
POOL = redis.ConnectionPool(host='localhost', port=6379, db=0)
NUSERS = 10000
NTAGS = 500
NBUCKETS = 1000
def fill(r):
p = r.pipeline()
for id in range(0,NUSERS):
user = "user:%d" % id
p.sadd( "index:%d" % (id%NBUCKETS), id )
for x in range(0,20):
tag = "tag:%d" % (random.randint(0,NTAGS))
p.zincrby( user, tag, 1 )
if id % 1000 == 0:
p.execute()
print id
p.execute()
def iterate(r):
for x in range(0,NBUCKETS):
for id in r.smembers( "index:%d"%(x) ):
user = "user:%d" % int(id)
print user,r.zrevrangebyscore(user,"+inf","-inf", 0, 5, True )
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)复杂度以迭代所有项。唯一的缺点是密钥迭代顺序是随机的。
index:0 => set[user:1000, user:1001,... user:2000 ]
改为index:0 => set[ 1000, 1001, ... 2000 ]
,以利用整数集合的优势,但除此之外,这是一个很好的策略! - Sripathi Krishnan