Redis
 sql >> Base de Dados >  >> NoSQL >> Redis

Conjuntos classificados Redis e melhor maneira de armazenar uids


Meu primeiro ponto seria observar que 4 GB são apertados para armazenar 20 milhões de conjuntos classificados. Uma tentativa rápida mostra que 20 milhões de usuários, cada um com 20 tags, levariam cerca de 8 GB em uma caixa de 64 bits (e é responsável pelas otimizações de memória ziplist do conjunto classificado fornecidas com o Redis 2.4 - nem tente isso com versões anteriores) .

Os conjuntos ordenados são a estrutura de dados ideal para dar suporte ao seu caso de uso. Eu usaria exatamente como você descreveu.

Como você apontou, KEYS não pode ser usado para iterar em chaves. É mais um comando de depuração. Para dar suporte à iteração de chave, você precisa adicionar uma estrutura de dados para fornecer esse caminho de acesso. As únicas estruturas no Redis que podem suportar iteração são a lista e o conjunto ordenado (através dos métodos de intervalo). No entanto, eles tendem a transformar algoritmos de iteração O(n) em O(n^2) (para lista) ou O(nlogn) (para zset). Uma lista também é uma má escolha para armazenar chaves, pois será difícil mantê-la à medida que as chaves são adicionadas/removidas.

Uma solução mais eficiente é adicionar um índice composto por conjuntos regulares. Você precisa usar uma função de hash para associar um usuário específico a um bucket e adicionar o ID do usuário ao conjunto correspondente a esse bucket. Se o ID do usuário for valores numéricos, uma função de módulo simples será suficiente. Se não forem, uma função de hashing de string simples fará o truque.

Então, para dar suporte à iteração em user:1000, user:2000 e user:1001, vamos escolher uma função de módulo 1000. user:1000 e user:2000 serão colocados no bucket index:0 enquanto user:1001 será colocado no bucket index:1.

Então, além dos zsets, agora temos as seguintes chaves:
index:0 => set[ 1000, 2000 ]
index:1 => set[ 1001 ]

Nos conjuntos, o prefixo das chaves não é necessário e permite que o Redis otimize o consumo de memória serializando os conjuntos desde que sejam mantidos pequenos o suficiente (otimização de conjuntos inteiros proposta por Sripathi Krishnan).

A iteração global consiste em um loop simples nos buckets de 0 a 1000 (excluídos). Para cada bucket, o comando SMEMBERS é aplicado para recuperar o conjunto correspondente e o cliente pode iterar nos itens individuais.

Aqui está um exemplo em 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()

Ajustando as constantes, você também pode usar este programa para avaliar o consumo de memória global dessa estrutura de dados.

IMO esta estratégia é simples e eficiente, porque oferece complexidade O(1) para adicionar/remover usuários e complexidade O(n) verdadeira para iterar em todos os itens. A única desvantagem é que a ordem de iteração da chave é aleatória.