Stringhe Redis vs hash Redis per rappresentare JSON: efficienza?


287

Voglio archiviare un payload JSON in redis. Ci sono davvero 2 modi in cui posso farlo:

  1. Uno usando una semplice stringa di chiavi e valori.
    chiave: utente, valore: payload (l'intero BLOB JSON che può essere 100-200 KB)

    SET user:1 payload

  2. Usando gli hash

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

Tieni presente che se uso un hash, la lunghezza del valore non è prevedibile. Non sono tutti brevi come l'esempio bio sopra.

Qual è la memoria più efficiente? Usando chiavi e valori stringa o usando un hash?


37
Tieni inoltre presente che non puoi (facilmente) archiviare un oggetto JSON nidificato in un set di hash.
Jonatan Hedborg,

3
ReJSON può aiutare anche qui: redislabs.com/blog/redis-as-a-json-store
Cihan B.

2
qualcuno ha usato ReJSON qui?
Swamy,

Risposte:


168

Dipende da come accedi ai dati:

Scegli l'opzione 1:

  • Se usi la maggior parte dei campi sulla maggior parte dei tuoi accessi.
  • Se c'è una variazione sui tasti possibili

Scegli l'opzione 2:

  • Se usi solo campi singoli sulla maggior parte dei tuoi accessi.
  • Se sai sempre quali campi sono disponibili

PS: come regola generale, scegli l'opzione che richiede meno query sulla maggior parte dei casi d'uso.


28
L'opzione 1 non è una buona idea se si prevede una modifica simultanea del JSONpayload (un classico problema non atomico read-modify-write ).
Samveen,

1
Quale è più efficiente tra le opzioni disponibili per l'archiviazione del BLOB JSON come stringa JSON o come array di byte in Redis?
Vinit89,

422

Questo articolo può fornire molte informazioni qui: http://redis.io/topics/memory-optimization

Esistono molti modi per memorizzare una matrice di oggetti in Redis ( spoiler : mi piace l'opzione 1 per la maggior parte dei casi d'uso):

  1. Memorizza l'intero oggetto come stringa codificata JSON in un'unica chiave e tieni traccia di tutti gli oggetti usando un set (o un elenco, se più appropriato). Per esempio:

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

    In generale, questo è probabilmente il metodo migliore nella maggior parte dei casi. Se ci sono molti campi nell'oggetto, i tuoi oggetti non sono nidificati con altri oggetti e tendi ad accedere solo a un piccolo sottoinsieme di campi alla volta, potrebbe essere meglio andare con l'opzione 2.

    Vantaggi : considerata una "buona pratica". Ogni oggetto è una chiave Redis in piena regola. L'analisi JSON è veloce, soprattutto quando è necessario accedere a molti campi per questo oggetto contemporaneamente. Svantaggi : più lento quando è necessario solo accedere a un singolo campo.

  2. Memorizza le proprietà di ciascun oggetto in un hash Redis.

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

    Vantaggi : considerata una "buona pratica". Ogni oggetto è una chiave Redis in piena regola. Non è necessario analizzare le stringhe JSON. Svantaggi : forse più lento quando è necessario accedere a tutti / la maggior parte dei campi in un oggetto. Inoltre, gli oggetti nidificati (Oggetti all'interno di oggetti) non possono essere facilmente memorizzati.

  3. Memorizza ogni oggetto come una stringa JSON in un hash Redis.

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

    Ciò consente di consolidare un po 'e utilizzare solo due chiavi anziché molte chiavi. L'ovvio svantaggio è che non è possibile impostare il TTL (e altre cose) su ciascun oggetto utente, poiché si tratta semplicemente di un campo nell'hash di Redis e non di una chiave Redis completa.

    Vantaggi : l'analisi JSON è veloce, soprattutto quando è necessario accedere a molti campi contemporaneamente per questo oggetto. Meno "inquinamento" dello spazio dei nomi chiave principale. svantaggi : circa lo stesso utilizzo della memoria del n. 1 quando si hanno molti oggetti. Più lento del n. 2 quando devi solo accedere a un singolo campo. Probabilmente non è considerata una "buona pratica".

  4. Memorizza ogni proprietà di ciascun oggetto in una chiave dedicata.

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

    Secondo l'articolo sopra, questa opzione non è quasi mai preferita (a meno che la proprietà dell'Oggetto non abbia un TTL specifico o qualcosa del genere).

    Vantaggi : le proprietà degli oggetti sono chiavi Redis complete, che potrebbero non essere eccessive per la tua app. Svantaggi : lento, utilizza più memoria e non considerato "best practice". Un sacco di inquinamento del principale spazio dei nomi chiave.

Sommario complessivo

L'opzione 4 non è generalmente preferita. Le opzioni 1 e 2 sono molto simili e sono entrambe abbastanza comuni. Preferisco l'opzione 1 (in generale) perché ti consente di memorizzare oggetti più complicati (con più livelli di annidamento, ecc.) L'opzione 3 viene utilizzata quando ti interessa davvero non inquinare lo spazio dei nomi delle chiavi principale (cioè non vuoi lì essere un sacco di chiavi nel tuo database e non ti importa di cose come TTL, key sharding o altro).

Se ho sbagliato qualcosa, ti preghiamo di considerare di lasciare un commento e di consentirmi di rivedere la risposta prima di procedere al downgrade. Grazie! :)


4
Per l'opzione n. 2, si dice "probabilmente più lento quando è necessario accedere a tutti / la maggior parte dei campi in un oggetto". Questo è stato testato?
mikegreiling

4
hmget è O (n) per n campi get con l'opzione 1 sarebbe comunque O (1). Teoricamente, sì, è più veloce.
Aruna Herath,

4
Che ne dici di combinare le opzioni 1 e 2 con un hash? Utilizzare l'opzione 1 per i dati aggiornati di rado e l'opzione 2 per i dati aggiornati di frequente? Ad esempio, stiamo memorizzando articoli e archiviamo campi come titolo, autore e URL in una stringa JSON con una chiave generica come obje archiviamo campi come visualizzazioni, voti e votanti con chiavi separate? In questo modo con una singola query READ ottieni l'intero oggetto e puoi comunque aggiornare rapidamente porzioni dinamiche dell'oggetto? Gli aggiornamenti relativamente rari ai campi nella stringa JSON possono essere eseguiti leggendo e riscrivendo l'intero oggetto in una transazione.
arun

2
Secondo questo: ( instagram-engineering.tumblr.com/post/12202313862/… ) si consiglia di archiviare in più hash in termini di consumo di memoria. Quindi, dopo l'ottimizzazione di Arun, possiamo fare: 1- fare più hash archiviando il payload json come stringhe per i dati aggiornati di rado, e 2- fare più hash archiviando i campi json per i dati aggiornati frequentemente
Aboelnour

2
Nel caso dell'opzione 1, perché lo stiamo aggiungendo a un set? Perché non possiamo semplicemente usare il comando Get e verificare se restituire non zero.
Pragmatico,

8

Alcune aggiunte a una determinata serie di risposte:

Prima di tutto, se hai intenzione di utilizzare l'hash Redis in modo efficiente, devi conoscere le chiavi che contano il numero massimo e i valori della dimensione massima, altrimenti se scompaiono hash-max-ziplist-value o hash-max-ziplist-entry, Redis lo convertirà in praticamente solite coppie chiave / valore sotto un cofano. (vedi hash-max-ziplist-value, hash-max-ziplist-entry) E rompere sotto una cappa dalle opzioni di hash È DAVVERO MALE, perché ogni normale coppia chiave / valore all'interno di Redis utilizza +90 byte per coppia.

Significa che se inizi con l'opzione due e rompi accidentalmente il valore max-hash-ziplist-otterrai +90 byte per OGNI ATTRIBUTO che hai all'interno del modello utente! (in realtà non il +90 ma +70 vedi l'output della console di seguito)

 # 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    
           ....
    }

Per la risposta di TheHippo, i commenti sull'opzione 1 sono fuorvianti:

hgetall / hmset / hmget in soccorso se sono necessari tutti i campi o più operazioni get / set.

Per la risposta BMiner.

La terza opzione è davvero divertente, per il set di dati con max (id) <ha-max-ziplist-value questa soluzione ha una complessità O (N), perché, sorpresa, Reddis memorizza piccoli hash come contenitore array-like di lunghezza / chiave / valore oggetti!

Ma molte volte gli hash contengono solo pochi campi. Quando gli hash sono piccoli, invece, possiamo semplicemente codificarli in una struttura di dati O (N), come un array lineare con coppie di valori chiave con prefisso di lunghezza. Dato che lo facciamo solo quando N è piccolo, il tempo ammortizzato per i comandi HGET e HSET è ancora O (1): l'hash verrà convertito in una vera tabella hash non appena il numero di elementi che contiene aumenterà troppo

Ma non dovresti preoccuparti, romperai le voci hash-max-ziplist molto velocemente e qui andrai ora alla soluzione numero 1.

Molto probabilmente la seconda opzione passerà alla quarta soluzione sotto copertura, come afferma la domanda:

Tieni presente che se uso un hash, la lunghezza del valore non è prevedibile. Non sono tutti brevi come l'esempio bio sopra.

E come hai già detto: la quarta soluzione è sicuramente il +70 byte più costoso per ogni attributo.

Il mio suggerimento su come ottimizzare tale set di dati:

Hai due opzioni:

  1. Se non è possibile garantire la dimensione massima di alcuni attributi utente di quanto si vada per la prima soluzione e se la materia di memoria è fondamentale rispetto a comprimere json utente prima di archiviare in redis.

  2. Se riesci a forzare la dimensione massima di tutti gli attributi. Puoi quindi impostare hash-max-ziplist-entry / value e utilizzare gli hash come hash per rappresentazione dell'utente o come ottimizzazione della memoria hash da questo argomento di una guida Redis: https://redis.io/topics/memory-optimization e memorizza l'utente come stringa json. In entrambi i casi è anche possibile comprimere attributi utente lunghi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.