Redis strings vs Redis hashes para representar JSON: ¿eficiencia?

287

Quiero almacenar una carga útil JSON en redis. Realmente hay 2 formas en que puedo hacer esto:

  1. Uno usando una cadena simple de claves y valores.
    clave: usuario, valor: carga útil (el blob JSON completo que puede ser de 100-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"

Tenga en cuenta que si uso un hash, la longitud del valor no es predecible. No todos son cortos, como el ejemplo biográfico anterior.

¿Cuál es más eficiente en memoria? ¿Usando cadenas y valores, o usando un hash?

Henley Chiu
fuente
37
También tenga en cuenta que no puede (fácilmente) almacenar un objeto JSON anidado en un conjunto hash.
Jonatan Hedborg
3
ReJSON también puede ayudar aquí: redislabs.com/blog/redis-as-a-json-store
Cihan B.
2
¿Alguien usó ReJSON aquí?
Swamy

Respuestas:

168

Depende de cómo acceda a los datos:

Ir a la opción 1:

  • Si usa la mayoría de los campos en la mayoría de sus accesos.
  • Si hay variación en las claves posibles

Ir a la opción 2:

  • Si usa solo campos individuales en la mayoría de sus accesos.
  • Si siempre sabes qué campos están disponibles

PD: Como regla general, elija la opción que requiere menos consultas en la mayoría de sus casos de uso.

TheHippo
fuente
28
La opción 1 no es una buena idea si se espera una modificación concurrente de la JSONcarga útil (un problema clásico de no atómico read-modify-write ).
Samveen
1
¿Cuál es más eficiente entre las opciones disponibles de almacenar json blob como una cadena json o como una matriz de bytes en Redis?
Vinit89
422

Este artículo puede proporcionar mucha información aquí: http://redis.io/topics/memory-optimization

Hay muchas formas de almacenar una matriz de objetos en Redis ( spoiler : me gusta la opción 1 para la mayoría de los casos de uso):

  1. Almacene todo el objeto como una cadena codificada con JSON en una sola clave y realice un seguimiento de todos los objetos utilizando un conjunto (o una lista, si es más apropiado). Por ejemplo:

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

    En términos generales, este es probablemente el mejor método en la mayoría de los casos. Si hay muchos campos en el Objeto, sus Objetos no están anidados con otros Objetos y tiende a acceder solo a un pequeño subconjunto de campos a la vez, podría ser mejor ir con la opción 2.

    Ventajas : considerada una "buena práctica". Cada objeto es una clave Redis completa. El análisis JSON es rápido, especialmente cuando necesita acceder a muchos campos para este Objeto a la vez. Desventajas : más lento cuando solo necesita acceder a un solo campo.

  2. Almacene las propiedades de cada Objeto en un hash Redis.

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

    Ventajas : considerada una "buena práctica". Cada objeto es una clave Redis completa. No es necesario analizar cadenas JSON. Desventajas : posiblemente más lento cuando necesita acceder a todos / la mayoría de los campos en un Objeto. Además, los objetos anidados (objetos dentro de objetos) no se pueden almacenar fácilmente.

  3. Almacene cada objeto como una cadena JSON en un hash de Redis.

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

    Esto le permite consolidar un poco y solo usar dos claves en lugar de muchas claves. La desventaja obvia es que no puede establecer el TTL (y otras cosas) en cada Objeto de usuario, ya que es simplemente un campo en el hash de Redis y no una clave Redis completa.

    Ventajas : el análisis JSON es rápido, especialmente cuando necesita acceder a muchos campos para este Objeto a la vez. Menos "contaminante" del espacio de nombres de la clave principal. Desventajas : aproximadamente el mismo uso de memoria que el n. ° 1 cuando tiene muchos objetos. Más lento que el n. ° 2 cuando solo necesita acceder a un solo campo. Probablemente no se considere una "buena práctica".

  4. Almacene cada propiedad de cada Objeto en una clave dedicada.

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

    De acuerdo con el artículo anterior, esta opción casi nunca se prefiere (a menos que la propiedad del Objeto deba tener TTL específico o algo así).

    Ventajas : las propiedades de los objetos son teclas Redis completas, que pueden no ser excesivas para su aplicación. Desventajas : lento, usa más memoria y no se considera "mejor práctica". Mucha contaminación del espacio de nombres de la clave principal.

Resumen total

La opción 4 generalmente no se prefiere. Las opciones 1 y 2 son muy similares, y ambas son bastante comunes. Prefiero la opción 1 (en términos generales) porque le permite almacenar objetos más complicados (con múltiples capas de anidamiento, etc.) La opción 3 se usa cuando realmente le importa no contaminar el espacio de nombres de la clave principal (es decir, no quiere allí ser muchas claves en su base de datos y no le importan cosas como TTL, fragmentación de claves o lo que sea).

Si tengo algo mal aquí, considere dejar un comentario y permitirme revisar la respuesta antes de votar. ¡Gracias! :)

BMiner
fuente
44
Para la Opción # 2, usted dice "posiblemente más lento cuando necesita acceder a todos / la mayoría de los campos en un Objeto". ¿Se ha probado esto?
mikegreiling
44
hmget es O (n) para n campos obtener con la opción 1 todavía sería O (1). Teóricamente, sí, es más rápido.
Aruna Herath
44
¿Qué tal combinar las opciones 1 y 2 con un hash? ¿Utiliza la opción 1 para datos que se actualizan con poca frecuencia y la opción 2 para datos que se actualizan con frecuencia? Digamos, estamos almacenando artículos y almacenamos campos como título, autor y url en una cadena JSON con una clave genérica como objy almacenamos campos como vistas, votos y votantes con claves separadas. De esta manera, con una sola consulta READ, obtiene todo el objeto y aún puede actualizar rápidamente partes dinámicas de su objeto. Las actualizaciones relativamente poco frecuentes de los campos en la cadena JSON se pueden hacer leyendo y escribiendo todo el objeto en una transacción.
Arun
2
De acuerdo con esto: ( instagram-engineering.tumblr.com/post/12202313862/… ) se recomienda almacenar en múltiples hashes en términos de consumo de memoria. Entonces, después de la optimización de arun, podemos hacer: 1- hacer múltiples hashes que almacenan la carga útil json como cadenas para los datos actualizados con poca frecuencia, y 2- hacer múltiples hashes que almacenan los campos json para los datos actualizados con frecuencia
Aboelnour
2
En el caso de la opción 1, ¿por qué lo estamos agregando a un conjunto? ¿Por qué no podemos simplemente usar el comando Get y verificar si return in no nil?
Pragmático
8

Algunas adiciones a un conjunto dado de respuestas:

En primer lugar, si va a usar Redis hash de manera eficiente, debe saber el número máximo de claves y el tamaño máximo de los valores; de lo contrario, si rompen hash-max-ziplist-value o hash-max-ziplist-entries Redis lo convertirá prácticamente pares clave / valor habituales debajo de un capó. (vea hash-max-ziplist-value, hash-max-ziplist-ingreses) Y romper bajo un capó de las opciones hash ES REALMENTE MALO, porque cada par clave / valor habitual dentro de Redis usa +90 bytes por par.

¡Esto significa que si comienza con la opción dos y sale accidentalmente de max-hash-ziplist-value obtendrá +90 bytes por CADA ATRIBUTO que tenga dentro del modelo de usuario! (en realidad no es el +90 sino el +70 ver la salida de la consola a continuación)

 # 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 la respuesta de TheHippo, los comentarios sobre la Opción uno son engañosos:

hgetall / hmset / hmget al rescate si necesita todos los campos o múltiples operaciones get / set.

Para respuesta BMiner.

La tercera opción es realmente divertida, para el conjunto de datos con max (id) <has-max-ziplist-value, esta solución tiene complejidad O (N), porque, sorpresa, Reddis almacena pequeños hashes como contenedores de longitud / clave / valor ¡objetos!

Pero muchas veces los hashes contienen solo unos pocos campos. Cuando los hashes son pequeños, podemos codificarlos en una estructura de datos O (N), como una matriz lineal con pares de valores de clave con prefijo de longitud. Dado que hacemos esto solo cuando N es pequeño, el tiempo amortizado para los comandos HGET y HSET sigue siendo O (1): el hash se convertirá en una tabla hash real tan pronto como el número de elementos que contiene crezca demasiado

Pero no debe preocuparse, romperá las entradas de hash-max-ziplist muy rápido y allí está, en realidad, ahora está en la solución número 1.

La segunda opción probablemente irá a la cuarta solución bajo una capucha porque, como dice la pregunta:

Tenga en cuenta que si uso un hash, la longitud del valor no es predecible. No todos son cortos, como el ejemplo biográfico anterior.

Y como ya dijo: la cuarta solución es el byte +70 más caro por cada atributo seguro.

Mi sugerencia de cómo optimizar dicho conjunto de datos:

Tienes dos opciones:

  1. Si no puede garantizar el tamaño máximo de algunos atributos del usuario, busque la primera solución y si la memoria es crucial, comprima el usuario json antes de almacenarlo en redis.

  2. Si puede forzar el tamaño máximo de todos los atributos. Luego, puede establecer hash-max-ziplist-entries / value y usar hash como un hash por representación de usuario O como optimización de memoria hash de este tema de una guía de Redis: https://redis.io/topics/memory-optimization y almacenar usuario como cadena json. De cualquier manera, también puede comprimir atributos de usuario largos.

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