Ciągi Redis kontra skróty Redis do reprezentowania JSON: wydajność?

287

Chcę zapisać ładunek JSON w Redis. Są naprawdę 2 sposoby, aby to zrobić:

  1. Jeden za pomocą prostych kluczy łańcuchowych i wartości.
    klucz: użytkownik, wartość: ładunek (cały obiekt blob JSON, który może wynosić 100-200 KB)

    SET user:1 payload

  2. Używanie skrótów

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

Pamiętaj, że jeśli użyję skrótu, długość wartości nie jest przewidywalna. Nie wszystkie są krótkie, tak jak powyższy przykład biografii.

Która jest bardziej wydajna pamięć? Używasz kluczy i wartości ciągów, czy używasz skrótu?

Henley Chiu
źródło
37
Należy również pamiętać, że nie można (łatwo) przechowywać zagnieżdżonego obiektu JSON w zestawie skrótów.
Jonatan Hedborg
3
ReJSON może tu również pomóc: redislabs.com/blog/redis-as-a-json-store
Cihan B.
2
czy ktoś tutaj używał ReJSON?
Swamy

Odpowiedzi:

168

To zależy od sposobu dostępu do danych:

Przejdź do opcji 1:

  • Jeśli używasz większości pól w większości swoich dostępów.
  • Jeśli istnieje różnica w możliwych kluczach

Przejdź do opcji 2:

  • Jeśli używasz tylko pojedynczych pól na większości swoich dostępów.
  • Jeśli zawsze wiesz, które pola są dostępne

PS: Z reguły wybierz opcję, która wymaga mniej zapytań w większości przypadków użycia.

TheHippo
źródło
28
Wariant 1 nie jest dobrym pomysłem, jeśli równoczesna zmiana na JSONOczekuje ładowności (klasyczny problem non-atomowych read-modify-write ).
Samveen,
1
Która opcja jest bardziej wydajna spośród dostępnych opcji przechowywania obiektu blob json jako ciągu json lub tablicy bajtów w Redis?
Vinit89,
422

Ten artykuł może dostarczyć wiele wglądu tutaj: http://redis.io/topics/memory-optimization

Istnieje wiele sposobów przechowywania tablicy obiektów w Redis ( spoiler : Podoba mi się opcja 1 dla większości przypadków użycia):

  1. Przechowuj cały obiekt jako ciąg zakodowany w JSON w jednym kluczu i śledź wszystkie Obiekty za pomocą zestawu (lub listy, jeśli bardziej odpowiednie). Na przykład:

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

    Ogólnie rzecz biorąc, jest to prawdopodobnie najlepsza metoda w większości przypadków. Jeśli w obiekcie jest wiele pól, twoje obiekty nie są zagnieżdżone z innymi obiektami i masz tendencję do uzyskiwania dostępu tylko do niewielkiego podzbioru pól na raz, lepiej wybrać opcję 2.

    Zalety : uważane za „dobrą praktykę”. Każdy obiekt jest pełnowymiarowym kluczem Redis. Analiza JSON jest szybka, szczególnie gdy potrzebujesz uzyskać dostęp do wielu pól dla tego obiektu jednocześnie. Wady : wolniej, gdy potrzebujesz tylko dostępu do jednego pola.

  2. Przechowuj właściwości każdego obiektu w skrócie Redis.

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

    Zalety : uważane za „dobrą praktykę”. Każdy obiekt jest pełnowymiarowym kluczem Redis. Nie trzeba analizować ciągów JSON. Wady : być może wolniej, gdy trzeba uzyskać dostęp do wszystkich / większości pól w obiekcie. Ponadto zagnieżdżone obiekty (obiekty w obiektach) nie mogą być łatwo przechowywane.

  3. Przechowuj każdy obiekt jako ciąg JSON w skrócie Redis.

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

    To pozwala ci trochę konsolidować i używać tylko dwóch kluczy zamiast wielu kluczy. Oczywistą wadą jest to, że nie można ustawić TTL (i innych rzeczy) na każdym obiekcie użytkownika, ponieważ jest to tylko pole w skrócie Redis, a nie pełnoprawny klucz Redis.

    Zalety : parsowanie JSON jest szybkie, szczególnie gdy trzeba uzyskać dostęp do wielu pól dla tego obiektu jednocześnie. Mniej „zanieczyszczania” głównej przestrzeni nazw kluczy. Wady : mniej więcej takie samo zużycie pamięci jak nr 1, gdy masz dużo obiektów. Wolniej niż # 2, gdy potrzebujesz uzyskać dostęp tylko do jednego pola. Prawdopodobnie nie uważany za „dobrą praktykę”.

  4. Przechowuj każdą właściwość każdego obiektu w dedykowanym kluczu.

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

    Zgodnie z powyższym artykułem ta opcja prawie nigdy nie jest preferowana (chyba że właściwość Object musi mieć określony TTL lub coś takiego).

    Zalety : Właściwości obiektu są pełnowymiarowymi kluczami Redis, co może nie być przesadą dla twojej aplikacji. Wady : powolne, zużywa więcej pamięci i nie jest uważane za „najlepszą praktykę”. Dużo zanieczyszczeń w głównej przestrzeni nazw kluczy.

Ogólne podsumowanie

Opcja 4 zasadniczo nie jest preferowana. Opcje 1 i 2 są bardzo podobne i oba są dość powszechne. Wolę opcję 1 (ogólnie), ponieważ pozwala ona na przechowywanie bardziej skomplikowanych obiektów (z wieloma warstwami zagnieżdżania itp.) Opcja 3 jest używana, gdy naprawdę zależy Ci na nie zanieczyszczaniu głównej przestrzeni nazw kluczy (tzn. Nie chcesz tam być dużą ilością kluczy w bazie danych i nie przejmujesz się takimi sprawami jak TTL, dzielenie kluczy itp.).

Jeśli coś mi się tutaj nie zgadza, rozważ zostawienie komentarza i pozwól mi zrewidować odpowiedź przed oddaniem głosu. Dzięki! :)

BMiner
źródło
4
W przypadku Opcji 2 mówisz „być może wolniej, gdy potrzebujesz uzyskać dostęp do wszystkich / większości pól w obiekcie”. Czy zostało to przetestowane?
mikegreiling
4
hmget to O (n) dla n pól otrzymanych z opcją 1 nadal będzie O (1). Teoretycznie tak, jest szybszy.
Aruna Herath
4
Co powiesz na połączenie opcji 1 i 2 z hashem? Czy skorzystać z opcji 1 w przypadku rzadko aktualizowanych danych i opcji 2 w przypadku często aktualizowanych danych? Powiedzmy, że przechowujemy artykuły i przechowujemy pola takie jak tytuł, autor i adres URL w ciągu JSON z ogólnym kluczem jak obji przechowujemy pola takie jak widoki, głosy i wyborcy z osobnymi kluczami? W ten sposób za pomocą jednego zapytania READ otrzymujesz cały obiekt i nadal możesz szybko aktualizować dynamiczne części swojego obiektu? Względnie rzadkie aktualizacje pól w ciągu JSON można wykonać, odczytując i zapisując cały obiekt z powrotem w transakcji.
arun
2
Zgodnie z tym: ( instagram-engineering.tumblr.com/post/12202313862/... ) zaleca się przechowywanie wielu skrótów pod względem zużycia pamięci. Tak więc po optymalizacji arun możemy: 1. wykonać wiele skrótów przechowujących ładunek json jako ciągi danych rzadko aktualizowanych danych i 2- zrobić wiele skrótów przechowujących pola json dla często aktualizowanych danych
Aboelnour
2
W przypadku opcji 1, dlaczego dodajemy ją do zestawu? Dlaczego nie możemy po prostu użyć polecenia Get i sprawdzić, czy zwrot nie jest zerowy.
Pragmatic,
8

Niektóre dodatki do danego zestawu odpowiedzi:

Przede wszystkim, jeśli zamierzasz efektywnie korzystać z skrótu Redis, musisz wiedzieć, że klucze liczą maksymalną liczbę i wartości maksymalny rozmiar - w przeciwnym razie, jeśli wyłamią wartości skrótu-wartości-ziplista lub wartości skrótu-z-listy-kluczy, Redis przekonwertuje je na praktycznie zwykłe pary klucz / wartość pod maską. .

Oznacza to, że jeśli zaczniesz z opcją drugą i przypadkowo wyrwiesz się z wartości maksymalnej-z-listy skrótów, otrzymasz +90 bajtów na KAŻDY ATRYBUT, który masz wewnątrz modelu użytkownika! (właściwie nie +90, ale +70 patrz wyjście konsoli poniżej)

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

W przypadku odpowiedzi TheHippo komentarze do Opcji pierwszej wprowadzają w błąd:

hgetall / hmset / hmget na ratunek, jeśli potrzebujesz wszystkich pól lub wielu operacji get / set.

Na odpowiedź BMiner.

Trzecia opcja jest naprawdę fajna, ponieważ zestaw danych z wartością max (id) <has-max-ziplist-value to rozwiązanie ma złożoność O (N), ponieważ, niespodzianka, Reddis przechowuje małe skróty jako podobny do tablicy kontener długości / klucza / wartości przedmioty!

Ale wiele razy skróty zawierają tylko kilka pól. Gdy skróty są małe, możemy zamiast tego po prostu zakodować je w strukturze danych O (N), jak tablica liniowa z parami klucz-wartość z prefiksem długości. Ponieważ robimy to tylko wtedy, gdy N jest małe, zamortyzowany czas dla poleceń HGET i HSET wynosi wciąż O (1): skrót zostanie przekonwertowany na prawdziwą tablicę skrótów, gdy tylko liczba zawartych w nim elementów wzrośnie zbyt mocno

Ale nie powinieneś się martwić, bardzo szybko przerwiesz wpisy hash-max-ziplist-i gotowe, jesteś teraz przy rozwiązaniu numer 1.

Druga opcja najprawdopodobniej przejdzie do czwartego rozwiązania pod maską, ponieważ jak stanowi pytanie:

Pamiętaj, że jeśli użyję skrótu, długość wartości nie jest przewidywalna. Nie wszystkie są krótkie, tak jak powyższy przykład biografii.

I jak już powiedziałeś: czwarte rozwiązanie to na pewno najdroższy bajt +70 na każdy atrybut.

Moja sugestia jak zoptymalizować taki zestaw danych:

Masz dwie opcje:

  1. Jeśli nie możesz zagwarantować maksymalnego rozmiaru niektórych atrybutów użytkownika, przejdź do pierwszego rozwiązania i jeśli ważna jest pamięć, to skompresuj użytkownika json przed przechowywaniem w redis.

  2. Jeśli możesz wymusić maksymalny rozmiar wszystkich atrybutów. Następnie możesz ustawić hash-max-ziplist-wpisy / wartość i używać skrótów jako jeden skrót dla reprezentacji użytkownika LUB jako optymalizację pamięci hash z tego tematu przewodnika Redis: https://redis.io/topics/memory-optimization i przechowuj użytkownika jako ciąg json. Tak czy inaczej możesz kompresować długie atrybuty użytkownika.

Алексей Лещук
źródło