Mijn eerste punt zou zijn om op te merken dat 4 GB krap is om 20 miljoen gesorteerde sets op te slaan. Een snelle poging laat zien dat 20 miljoen gebruikers, elk met 20 tags, ongeveer 8 GB nodig zouden hebben op een 64-bits box (en het is goed voor de gesorteerde set ziplist-geheugenoptimalisaties geleverd met Redis 2.4 - probeer dit niet eens met eerdere versies) .
Gesorteerde sets zijn de ideale datastructuur om uw use case te ondersteunen. Ik zou ze precies gebruiken zoals je hebt beschreven.
Zoals u aangaf, kunnen KEYS niet worden gebruikt om sleutels te herhalen. Het is eerder bedoeld als een debug-opdracht. Om sleuteliteratie te ondersteunen, moet u een gegevensstructuur toevoegen om dit toegangspad te bieden. De enige structuren in Redis die iteratie kunnen ondersteunen, zijn de lijst en de gesorteerde set (via de bereikmethoden). Ze hebben echter de neiging om O (n) iteratie-algoritmen om te zetten in O (n ^ 2) (voor lijst) of O (nlogn) (voor zset). Een lijst is ook een slechte keuze om sleutels op te slaan, omdat het moeilijk zal zijn om deze te onderhouden als sleutels worden toegevoegd/verwijderd.
Een efficiëntere oplossing is om een index toe te voegen die is samengesteld uit reguliere sets. U moet een hashfunctie gebruiken om een specifieke gebruiker aan een bucket te koppelen en de gebruikers-ID toevoegen aan de set die overeenkomt met deze bucket. Als de gebruikers-ID numerieke waarden zijn, is een eenvoudige modulo-functie voldoende. Als dat niet het geval is, is een eenvoudige hashfunctie voor strings voldoende.
Dus om iteratie op gebruiker:1000, gebruiker:2000 en gebruiker:1001 te ondersteunen, kiezen we een modulo 1000-functie. user:1000 en user:2000 worden in bucket index:0 geplaatst, terwijl user:1001 in bucket index:1 worden geplaatst.
Dus bovenop de zsets hebben we nu de volgende sleutels:
index:0 => set[ 1000, 2000 ]
index:1 => set[ 1001 ]
In de sets is het voorvoegsel van de sleutels niet nodig, en het stelt Redis in staat het geheugenverbruik te optimaliseren door de sets te serialiseren, op voorwaarde dat ze klein genoeg worden gehouden (optimalisatie van gehele sets voorgesteld door Sripathi Krishnan).
De globale iteratie bestaat uit een eenvoudige lus op de buckets van 0 tot 1000 (exclusief). Voor elke bucket wordt het SMEMBERS-commando toegepast om de bijbehorende set op te halen, en de klant kan vervolgens de afzonderlijke items herhalen.
Hier is een voorbeeld in 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()
Door de constanten aan te passen, kunt u dit programma ook gebruiken om het globale geheugengebruik van deze gegevensstructuur te evalueren.
IMO is deze strategie eenvoudig en efficiënt, omdat het O(1) complexiteit biedt om gebruikers toe te voegen/te verwijderen, en echte O(n) complexiteit om op alle items te herhalen. Het enige nadeel is dat de volgorde van de iteratie willekeurig is.