Redis strings vs Redis hashes para representar JSON: eficiência?

287

Quero armazenar uma carga JSON em redis. Existem realmente duas maneiras de fazer isso:

  1. Um usando chaves e valores simples de string.
    key: user, value: payload (todo o blob JSON que pode ter entre 100 e 200 KB)

    SET user:1 payload

  2. Usando hashes

    HSET user:1 username "someone"
    HSET user:1 location "NY"
    HSET user:1 bio "STRING WITH OVER 100 lines"

Lembre-se de que, se eu usar um hash, o tamanho do valor não será previsível. Eles não são todos curtos, como o exemplo bio acima.

Qual é mais eficiente em termos de memória? Usando chaves e valores de string ou usando um hash?

Henley Chiu
fonte
37
Lembre-se também de que não é possível (facilmente) armazenar um objeto JSON aninhado em um conjunto de hash.
Jonatan Hedborg 5/05
3
ReJSON também pode ajudar aqui: redislabs.com/blog/redis-as-a-json-store #
Cihan B.
2
alguém usou ReJSON aqui?
Swamy

Respostas:

168

Depende de como você acessa os dados:

Vá para a opção 1:

  • Se você usa a maioria dos campos na maioria dos seus acessos.
  • Se houver variação nas possíveis chaves

Vá para a opção 2:

  • Se você usar apenas campos únicos na maioria dos seus acessos.
  • Se você sempre sabe quais campos estão disponíveis

PS: Como regra geral, escolha a opção que requer menos consultas na maioria dos seus casos de uso.

TheHippo
fonte
28
A opção 1 não é uma boa ideia se se espera uma modificação simultânea da JSONcarga útil (um problema clássico de não atômica read-modify-write ).
Samveen
1
Qual é mais eficiente entre as opções disponíveis de armazenamento do json blob como uma string json ou como uma matriz de bytes no Redis?
Vinit89
422

Este artigo pode fornecer muitas informações aqui: http://redis.io/topics/memory-optimization

Existem várias maneiras de armazenar uma variedade de objetos no Redis ( spoiler : gosto da opção 1 na maioria dos casos de uso):

  1. Armazene o objeto inteiro como uma string codificada em JSON em uma única chave e acompanhe todos os Objetos usando um conjunto (ou lista, se mais apropriado). Por exemplo:

    INCR id:users
    SET user:{id} '{"name":"Fred","age":25}'
    SADD users {id}

    De um modo geral, este é provavelmente o melhor método na maioria dos casos. Se houver muitos campos no Objeto, seus Objetos não estão aninhados com outros Objetos e você tende a acessar apenas um pequeno subconjunto de campos por vez, talvez seja melhor seguir a opção 2.

    Vantagens : considerada uma "boa prática". Cada objeto é uma chave Redis completa. A análise JSON é rápida, especialmente quando você precisa acessar muitos campos para este Objeto de uma só vez. Desvantagens : mais lento quando você precisa acessar apenas um único campo.

  2. Armazene as propriedades de cada objeto em um hash Redis.

    INCR id:users
    HMSET user:{id} name "Fred" age 25
    SADD users {id}

    Vantagens : considerada uma "boa prática". Cada objeto é uma chave Redis completa. Não há necessidade de analisar cadeias JSON. Desvantagens : possivelmente mais lento quando você precisar acessar todos / a maioria dos campos em um Objeto. Além disso, objetos aninhados (objetos dentro de objetos) não podem ser facilmente armazenados.

  3. Armazene cada Objeto como uma sequência JSON em um hash Redis.

    INCR id:users
    HMSET users {id} '{"name":"Fred","age":25}'

    Isso permite que você consolide um pouco e use apenas duas chaves em vez de muitas chaves. A desvantagem óbvia é que você não pode definir o TTL (e outras coisas) em cada objeto de usuário, pois é apenas um campo no hash Redis e não uma chave Redis completa.

    Vantagens : A análise JSON é rápida, especialmente quando você precisa acessar muitos campos para este Objeto de uma só vez. Menos "poluidor" do espaço para nome da chave principal. Desvantagens : Sobre o mesmo uso de memória que o nº 1, quando você tem muitos objetos. Mais lento que o número 2 quando você precisa acessar apenas um único campo. Provavelmente não é considerado uma "boa prática".

  4. Armazene cada propriedade de cada objeto em uma chave dedicada.

    INCR id:users
    SET user:{id}:name "Fred"
    SET user:{id}:age 25
    SADD users {id}

    De acordo com o artigo acima, essa opção quase nunca é preferida (a menos que a propriedade do objeto precise ter TTL específico ou algo assim).

    Vantagens : as propriedades do objeto são chaves Redis completas, que podem não ser um exagero para o seu aplicativo. Desvantagens : lento, usa mais memória e não é considerado "melhor prática". Muitos poluidores do espaço para nome da chave principal.

Resumo geral

A opção 4 geralmente não é preferida. As opções 1 e 2 são muito semelhantes e são bastante comuns. Prefiro a opção 1 (de modo geral) porque permite armazenar objetos mais complicados (com várias camadas de aninhamento, etc.) A opção 3 é usada quando você realmente se preocupa em não poluir o espaço para nome da chave principal (ou seja, você não deseja ter muitas chaves em seu banco de dados e você não se importa com coisas como TTL, compartilhamento de chaves ou qualquer outra coisa).

Se eu entendi algo errado aqui, considere deixar um comentário e me permita revisar a resposta antes da votação. Obrigado! :)

BMiner
fonte
4
Para a opção 2, você diz "possivelmente mais lento quando precisar acessar todos / a maioria dos campos em um objeto". Isso foi testado?
Mikegreiling # 7/14
4
hmget é O (n), para n campos que a opção 1 ainda seria O (1). Teoricamente, sim, é mais rápido.
Aruna Herath
4
Que tal combinar as opções 1 e 2 com um hash? Use a opção 1 para dados atualizados com pouca frequência e a opção 2 para dados atualizados com frequência? Digamos, estamos armazenando artigos e armazenamos campos como título, autor e URL em uma string JSON com uma chave genérica como obje armazenamos campos como visualizações, votos e eleitores com chaves separadas? Dessa forma, com uma única consulta READ, você obtém o objeto inteiro e ainda pode atualizar partes dinâmicas do seu objeto rapidamente? As atualizações relativamente infreqüentes dos campos na cadeia JSON podem ser feitas lendo e gravando o objeto inteiro novamente em uma transação.
Arun
2
De acordo com o seguinte: ( instagram-engineering.tumblr.com/post/12202313862/… ), é recomendável armazenar em vários hashes em termos de consumo de memória. Portanto, após a otimização do arun, podemos fazer: 1 - criar vários hashes armazenando a carga útil do json como seqüências de caracteres para os dados atualizados com pouca frequência e 2 - criar vários hashes armazenando os campos json dos dados atualizados com freqüência
Aboelnour
2
No caso da opção 1, por que a estamos adicionando a um conjunto? Por que não podemos simplesmente usar o comando Get e verificar se o retorno é nulo?
Pragmatic
8

Algumas adições a um determinado conjunto de respostas:

Antes de tudo, se você usar o hash Redis de maneira eficiente, deverá saber que as teclas contam número máximo e valores tamanho máximo - caso contrário, se elas quebrarem as entradas hash-max-ziplist-value ou hash-max-ziplist-redis, o Redis o converterá em praticamente pares chave / valor usuais sob um capô. (consulte as entradas hash-max-ziplist, hash-max-ziplist) E quebrar sob o capô de uma opção de hash É MUITO RUIM, porque cada par de chave / valor usual dentro do Redis usa +90 bytes por par.

Isso significa que, se você começar com a opção dois e acidentalmente romper o valor de max-hash-ziplist, receberá +90 bytes por CADA ATRIBUTO que tiver dentro do modelo do usuário! (na verdade, não os +90, mas +70, veja a saída do console abaixo)

 # you need me-redis and awesome-print gems to run exact code
 redis = Redis.include(MeRedis).configure( hash_max_ziplist_value: 64, hash_max_ziplist_entries: 512 ).new 
  => #<Redis client v4.0.1 for redis://127.0.0.1:6379/0> 
 > redis.flushdb
  => "OK" 
 > ap redis.info(:memory)
    {
                "used_memory" => "529512",
          **"used_memory_human" => "517.10K"**,
            ....
    }
  => nil 
 # me_set( 't:i' ... ) same as hset( 't:i/512', i % 512 ... )    
 # txt is some english fictionary book around 56K length, 
 # so we just take some random 63-symbols string from it 
 > redis.pipelined{ 10000.times{ |i| redis.me_set( "t:#{i}", txt[rand(50000), 63] ) } }; :done
 => :done 
 > ap redis.info(:memory)
  {
               "used_memory" => "1251944",
         **"used_memory_human" => "1.19M"**, # ~ 72b per key/value
            .....
  }
  > redis.flushdb
  => "OK" 
  # setting **only one value** +1 byte per hash of 512 values equal to set them all +1 byte 
  > redis.pipelined{ 10000.times{ |i| redis.me_set( "t:#{i}", txt[rand(50000), i % 512 == 0 ? 65 : 63] ) } }; :done 
  > ap redis.info(:memory)
   {
               "used_memory" => "1876064",
         "used_memory_human" => "1.79M",   # ~ 134 bytes per pair  
          ....
   }
    redis.pipelined{ 10000.times{ |i| redis.set( "t:#{i}", txt[rand(50000), 65] ) } };
    ap redis.info(:memory)
    {
             "used_memory" => "2262312",
          "used_memory_human" => "2.16M", #~155 byte per pair i.e. +90 bytes    
           ....
    }

Para a resposta TheHippo, os comentários sobre a opção um são enganosos:

hgetall / hmset / hmget para o resgate, se você precisar de todos os campos ou de várias operações get / set.

Para resposta BMiner.

A terceira opção é realmente muito divertida, para um conjunto de dados com valor máximo (id) <has-max-ziplist-value, esta solução possui complexidade O (N), porque, surpresa, o Reddis armazena pequenos hashes como contêineres de comprimento / chave / valor em forma de matriz objetos!

Mas muitas vezes os hashes contêm apenas alguns campos. Quando os hashes são pequenos, podemos apenas codificá-los em uma estrutura de dados O (N), como uma matriz linear com pares de valores-chave com prefixo de comprimento. Como fazemos isso apenas quando N é pequeno, o tempo amortizado para os comandos HGET e HSET ainda é O (1): o hash será convertido em uma tabela de hash real assim que o número de elementos que ele contiver aumentar demais

Mas não se preocupe, você quebrará as entradas hash-max-ziplist muito rápido e lá está você agora na solução número 1.

A segunda opção provavelmente irá para a quarta solução, porque, como afirma a pergunta:

Lembre-se de que, se eu usar um hash, o tamanho do valor não será previsível. Eles não são todos curtos, como o exemplo bio acima.

E como você já disse: a quarta solução é o mais caro +70 bytes por cada atributo, com certeza.

Minha sugestão de como otimizar esse conjunto de dados:

Você tem duas opções:

  1. Se você não puder garantir o tamanho máximo de alguns atributos do usuário, procure a primeira solução e se a questão da memória for crucial, comprima o usuário json antes de armazenar em redis.

  2. Se você pode forçar o tamanho máximo de todos os atributos. Você pode definir hash-max-ziplist-entradas / valor e usar hashes como um hash por representação do usuário OU como otimização de memória de hash neste tópico de um guia Redis: https://redis.io/topics/memory-optimization e armazenar usuário como string json. De qualquer forma, você também pode compactar atributos de usuário longos.

Алексей Лещук
fonte