Redis Strings vs Redis Hashes repräsentieren JSON: Effizienz?

287

Ich möchte eine JSON-Nutzlast in Redis speichern. Es gibt wirklich zwei Möglichkeiten, wie ich das tun kann:

  1. Eine mit einfachen String-Schlüsseln und Werten.
    Schlüssel: Benutzer, Wert: Nutzlast (der gesamte JSON-Blob, der 100-200 KB groß sein kann)

    SET user:1 payload

  2. Hashes verwenden

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

Denken Sie daran, dass die Wertelänge nicht vorhersehbar ist, wenn ich einen Hash verwende. Sie sind nicht alle kurz wie das obige Bio-Beispiel.

Was ist speichereffizienter? Verwenden Sie Zeichenfolgenschlüssel und -werte oder einen Hash?

Henley Chiu
quelle
37
Beachten Sie auch, dass Sie ein verschachteltes JSON-Objekt nicht (einfach) in einem Hash-Set speichern können.
Jonatan Hedborg
3
ReJSON kann auch hier helfen: redislabs.com/blog/redis-as-a-json-store
Cihan B.
2
hat hier jemand ReJSON benutzt?
Swamy

Antworten:

168

Dies hängt davon ab, wie Sie auf die Daten zugreifen:

Wählen Sie Option 1:

  • Wenn Sie die meisten Felder für die meisten Zugriffe verwenden.
  • Wenn es Abweichungen bei möglichen Schlüsseln gibt

Wählen Sie Option 2:

  • Wenn Sie bei den meisten Zugriffen nur einzelne Felder verwenden.
  • Wenn Sie immer wissen, welche Felder verfügbar sind

PS: Wählen Sie als Faustregel die Option, für die in den meisten Anwendungsfällen weniger Abfragen erforderlich sind.

TheHippo
quelle
28
Option 1 ist keine gute Idee, wenn eine gleichzeitige Änderung der JSONNutzlast erwartet wird (ein klassisches Problem der nichtatomaren read-modify-write ).
Samveen
1
Was ist effizienter unter den verfügbaren Optionen zum Speichern von JSON-Blobs als JSON-Zeichenfolge oder als Byte-Array in Redis?
Vinit89
422

Dieser Artikel bietet hier viele Einblicke: http://redis.io/topics/memory-optimization

Es gibt viele Möglichkeiten, ein Array von Objekten in Redis zu speichern ( Spoiler : Ich mag Option 1 für die meisten Anwendungsfälle):

  1. Speichern Sie das gesamte Objekt als JSON-codierte Zeichenfolge in einem einzigen Schlüssel und verfolgen Sie alle Objekte mithilfe eines Satzes (oder einer Liste, falls zutreffend). Beispielsweise:

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

    Im Allgemeinen ist dies in den meisten Fällen wahrscheinlich die beste Methode. Wenn das Objekt viele Felder enthält, Ihre Objekte nicht mit anderen Objekten verschachtelt sind und Sie in der Regel nur auf eine kleine Teilmenge von Feldern gleichzeitig zugreifen, ist es möglicherweise besser, Option 2 zu wählen.

    Vorteile : gilt als "gute Praxis". Jedes Objekt ist ein ausgewachsener Redis-Schlüssel. Die JSON-Analyse ist schnell, insbesondere wenn Sie auf viele Felder für dieses Objekt gleichzeitig zugreifen müssen. Nachteile : langsamer, wenn Sie nur auf ein einzelnes Feld zugreifen müssen.

  2. Speichern Sie die Eigenschaften jedes Objekts in einem Redis-Hash.

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

    Vorteile : gilt als "gute Praxis". Jedes Objekt ist ein ausgewachsener Redis-Schlüssel. JSON-Zeichenfolgen müssen nicht analysiert werden. Nachteile : Möglicherweise langsamer, wenn Sie auf alle / die meisten Felder in einem Objekt zugreifen müssen. Außerdem können verschachtelte Objekte (Objekte innerhalb von Objekten) nicht einfach gespeichert werden.

  3. Speichern Sie jedes Objekt als JSON-Zeichenfolge in einem Redis-Hash.

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

    Auf diese Weise können Sie ein wenig konsolidieren und statt vieler Schlüssel nur zwei Schlüssel verwenden. Der offensichtliche Nachteil ist, dass Sie die TTL (und andere Dinge) nicht für jedes Benutzerobjekt festlegen können, da es sich lediglich um ein Feld im Redis-Hash und nicht um einen vollständigen Redis-Schlüssel handelt.

    Vorteile : Die JSON-Analyse ist schnell, insbesondere wenn Sie auf viele Felder für dieses Objekt gleichzeitig zugreifen müssen. Weniger "Verschmutzung" des Hauptschlüssel-Namespace. Nachteile : Ungefähr die gleiche Speichernutzung wie bei Nummer 1, wenn Sie viele Objekte haben. Langsamer als # 2, wenn Sie nur auf ein einzelnes Feld zugreifen müssen. Wahrscheinlich nicht als "gute Praxis" angesehen.

  4. Speichern Sie jede Eigenschaft jedes Objekts in einem dedizierten Schlüssel.

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

    Gemäß dem obigen Artikel wird diese Option fast nie bevorzugt (es sei denn, die Eigenschaft des Objekts muss eine bestimmte TTL oder etwas anderes haben).

    Vorteile : Objekteigenschaften sind vollständige Redis-Schlüssel, die für Ihre App möglicherweise nicht übertrieben sind. Nachteile : Langsam, verbraucht mehr Speicher und wird nicht als "Best Practice" angesehen. Viel Verschmutzung des Hauptschlüssel-Namespace.

Gesamtübersicht

Option 4 wird im Allgemeinen nicht bevorzugt. Die Optionen 1 und 2 sind sehr ähnlich und beide sind ziemlich häufig. Ich bevorzuge Option 1 (im Allgemeinen), da Sie damit kompliziertere Objekte (mit mehreren Verschachtelungsebenen usw.) speichern können. Option 3 wird verwendet, wenn Sie wirklich daran interessiert sind, den Hauptschlüssel-Namespace nicht zu verschmutzen (dh Sie möchten dort nicht viele Schlüssel in Ihrer Datenbank zu sein und Sie interessieren sich nicht für Dinge wie TTL, Key Sharding oder was auch immer).

Wenn ich hier etwas falsch gemacht habe, hinterlasse bitte einen Kommentar und erlaube mir, die Antwort vor dem Downvoting zu überarbeiten. Vielen Dank! :) :)

BMiner
quelle
4
Für Option 2 sagen Sie "möglicherweise langsamer, wenn Sie auf alle / die meisten Felder in einem Objekt zugreifen müssen". Wurde dies getestet?
Mikegreiling
4
hmget ist O (n) für n Felder , die mit Option 1 erhalten werden, wäre immer noch O (1). Theoretisch ist es ja schneller.
Aruna Herath
4
Wie wäre es mit der Kombination der Optionen 1 und 2 mit einem Hash? Verwenden Sie Option 1 für selten aktualisierte Daten und Option 2 für häufig aktualisierte Daten? Angenommen, wir speichern Artikel und speichern Felder wie Titel, Autor und URL in einer JSON-Zeichenfolge mit einem generischen Schlüssel wie objund speichern Felder wie Ansichten, Stimmen und Wähler mit separaten Schlüsseln. Auf diese Weise erhalten Sie mit einer einzigen READ-Abfrage das gesamte Objekt und können dennoch dynamische Teile Ihres Objekts schnell aktualisieren? Die relativ seltenen Aktualisierungen von Feldern in der JSON-Zeichenfolge können durch Lesen und Zurückschreiben des gesamten Objekts in einer Transaktion erfolgen.
Arun
2
Demnach: ( instagram-engineering.tumblr.com/post/12202313862/… ) Es wird empfohlen, in Bezug auf den Speicherverbrauch in mehreren Hashes zu speichern. Nach der Optimierung von arun können wir also Folgendes tun: 1. mehrere Hashes erstellen, in denen die JSON-Nutzdaten als Zeichenfolgen für die selten aktualisierten Daten gespeichert werden, und 2- mehrere Hashes erstellen, in denen die JSON-Felder für die häufig aktualisierten Daten gespeichert werden
Aboelnour
2
Warum fügen wir Option1 einer Gruppe hinzu? Warum können wir nicht einfach den Befehl Get verwenden und prüfen, ob return nicht null ist?
Pragmatische
8

Einige Ergänzungen zu einer Reihe von Antworten:

Wenn Sie Redis-Hash effizient nutzen möchten, müssen Sie zunächst die maximale Anzahl der Schlüssel und die maximale Größe der Schlüssel kennen. Andernfalls konvertiert Redis den Hash-Max-Ziplist-Wert oder die Hash-Max-Ziplist-Einträge praktisch in übliche Schlüssel / Wert-Paare unter einer Haube. (siehe Hash-Max-Ziplist-Wert, Hash-Max-Ziplist-Einträge) Und das Unterbrechen einer Hash-Option unter einer Haube ist WIRKLICH SCHLECHT, da jedes übliche Schlüssel / Wert-Paar in Redis +90 Bytes pro Paar verwendet.

Dies bedeutet, dass Sie, wenn Sie mit Option 2 beginnen und versehentlich aus dem Max-Hash-Ziplist-Wert ausbrechen, +90 Bytes pro JEDEM ATTRIBUT erhalten, das Sie im Benutzermodell haben! (eigentlich nicht die +90 aber +70 siehe Konsolenausgabe unten)

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

Für die Antwort von TheHippo sind Kommentare zu Option 1 irreführend:

hgetall / hmset / hmget zur Rettung, wenn Sie alle Felder oder mehrere get / set-Operationen benötigen.

Für BMiner Antwort.

Die dritte Option macht wirklich Spaß. Für einen Datensatz mit max (id) <has-max-ziplist-value weist diese Lösung eine O (N) -Komplexität auf, da Reddis überraschenderweise kleine Hashes als Array-ähnlichen Container mit Länge / Schlüssel / Wert speichert Objekte!

Aber oft enthalten Hashes nur wenige Felder. Wenn Hashes klein sind, können wir sie stattdessen einfach in eine O (N) -Datenstruktur codieren, wie ein lineares Array mit Schlüsselwertpaaren mit Längenpräfix. Da wir dies nur tun, wenn N klein ist, beträgt die amortisierte Zeit für HGET- und HSET-Befehle immer noch O (1): Der Hash wird in eine echte Hash-Tabelle konvertiert, sobald die Anzahl der darin enthaltenen Elemente zu groß wird

Aber Sie sollten sich keine Sorgen machen, Sie werden Hash-Max-Ziplist-Einträge sehr schnell brechen und los geht's, Sie sind jetzt tatsächlich bei Lösung Nummer 1.

Die zweite Option wird höchstwahrscheinlich unter einer Haube zur vierten Lösung führen, da die Frage lautet:

Denken Sie daran, dass die Wertelänge nicht vorhersehbar ist, wenn ich einen Hash verwende. Sie sind nicht alle kurz wie das obige Bio-Beispiel.

Und wie Sie bereits sagten: Die vierte Lösung ist mit Sicherheit das teuerste +70 Byte pro Attribut.

Mein Vorschlag, wie man einen solchen Datensatz optimiert:

Sie haben zwei Möglichkeiten:

  1. Wenn Sie die maximale Größe einiger Benutzerattribute nicht garantieren können, entscheiden Sie sich für die erste Lösung, und wenn der Speicher wichtig ist, komprimieren Sie den Benutzer json, bevor Sie ihn in redis speichern.

  2. Wenn Sie die maximale Größe aller Attribute erzwingen können. Dann können Sie Hash-Max-Ziplist-Einträge / Wert festlegen und Hashes entweder als einen Hash pro Benutzerdarstellung oder als Hash-Speicheroptimierung aus diesem Thema eines Redis-Handbuchs verwenden: https://redis.io/topics/memory-optimization und Benutzer als JSON-Zeichenfolge speichern. In beiden Fällen können Sie auch lange Benutzerattribute komprimieren.

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