私の最初のポイントは、4GBは2000万のソートされたセットを格納するのに厳しいことに注意することです。簡単に試してみると、20Mのユーザーがそれぞれ20タグを使用すると、64ビットボックスで約8 GBが必要になります(Redis 2.4で提供されるソートされたセットのziplistメモリの最適化を考慮しています-以前のバージョンではこれを試さないでください) 。
並べ替えられたセットは、ユースケースをサポートするための理想的なデータ構造です。私はあなたが説明したとおりにそれらを使用します。
ご指摘のとおり、KEYSを使用してキーを反復処理することはできません。むしろデバッグコマンドとして意図されています。キーの反復をサポートするには、このアクセスパスを提供するデータ構造を追加する必要があります。反復をサポートできるRedisの構造は、リストとソートされたセット(範囲メソッドによる)のみです。ただし、O(n)反復アルゴリズムをO(n ^ 2)(リストの場合)またはO(nlogn)(zsetの場合)に変換する傾向があります。リストは、キーが追加/削除されるときにリストを維持するのが難しいため、キーを保存するのにも適していません。
より効率的な解決策は、通常のセットで構成されるインデックスを追加することです。ハッシュ関数を使用して特定のユーザーをバケットに関連付け、このバケットに対応するセットにユーザーIDを追加する必要があります。ユーザーIDが数値の場合、単純なモジュロ関数で十分です。そうでない場合は、単純な文字列ハッシュ関数でうまくいきます。
したがって、user:1000、user:2000、およびuser:1001での反復をサポートするために、モジュロ1000関数を選択しましょう。 user:1000とuser:2000はバケットインデックス:0に配置され、user:1001はバケットインデックス:1に配置されます。
したがって、zsetに加えて、次のキーがあります。
index:0 => set[ 1000, 2000 ]
index:1 => set[ 1001 ]
セットでは、キーのプレフィックスは不要であり、Redisは、セットが十分に小さく保たれている場合、セットをシリアル化することでメモリ消費を最適化できます(Sripathi Krishnanによって提案された整数セットの最適化)。
グローバル反復は、0から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()
定数を微調整することで、このプログラムを使用して、このデータ構造のグローバルメモリ消費量を評価することもできます。
IMOこの戦略は、ユーザーを追加/削除するためのO(1)の複雑さと、すべてのアイテムを反復するための真のO(n)の複雑さを提供するため、シンプルで効率的です。唯一の欠点は、キーの反復順序がランダムであることです。