Строки Redis против хэшей Redis для представления JSON: эффективность?

287

Я хочу сохранить полезную нагрузку JSON в Redis. Есть два способа сделать это:

  1. Один использует простые строковые ключи и значения.
    ключ: пользователь, значение: полезная нагрузка (весь двоичный объект JSON, который может быть 100-200 КБ)

    SET user:1 payload

  2. Использование хэшей

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

Имейте в виду, что если я использую хеш, длина значения не будет предсказуемой. Они не все короткие, как пример био выше.

Какая память более эффективна? Используя строковые ключи и значения, или используя хэш?

Хенли Чиу
источник
37
Также имейте в виду, что вы не можете (легко) хранить вложенный объект JSON в хэш-наборе.
Джонатан Хедборг
3
ReJSON может помочь и здесь: redislabs.com/blog/redis-as-a-json-store
Джихан Б.
2
кто-нибудь использовал ReJSON здесь?
Свами

Ответы:

168

Это зависит от того, как вы получаете доступ к данным:

Перейти к варианту 1:

  • Если вы используете большинство полей на большинстве ваших доступов.
  • Если есть различия на возможных клавишах

Перейти к варианту 2:

  • Если вы используете только отдельные поля в большинстве ваших обращений.
  • Если вы всегда знаете, какие поля доступны

PS: как правило, выберите вариант, который требует меньше запросов в большинстве случаев использования.

TheHippo
источник
28
Вариант 1 не является хорошей идеей , если одновременное изменение от JSONполезной нагрузки , как ожидается , (классическая проблема неатомическое read-modify-write ).
Самвин
1
Что является более эффективным среди доступных вариантов хранения json blob в виде строки json или байтового массива в Redis?
Vinit89
422

Эта статья может дать много понимания здесь: http://redis.io/topics/memory-optimization

Есть много способов сохранить массив объектов в Redis ( спойлер : мне нравится вариант 1 для большинства случаев использования):

  1. Сохраните весь объект как строку в кодировке JSON в одном ключе и отслеживайте все объекты, используя набор (или список, если это более уместно). Например:

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

    Вообще говоря, это, вероятно, лучший метод в большинстве случаев. Если в Объекте много полей, ваши Объекты не вложены в другие Объекты, и вы, как правило, получаете доступ только к небольшому подмножеству полей одновременно, может быть, лучше использовать вариант 2.

    Преимущества : считается «хорошей практикой». Каждый объект представляет собой полноценный ключ Redis. Синтаксический анализ JSON выполняется быстро, особенно если вам нужно получить доступ ко многим полям для этого объекта одновременно Недостатки : медленнее, когда вам нужно получить доступ только к одному полю.

  2. Сохраните свойства каждого объекта в хэше Redis.

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

    Преимущества : считается «хорошей практикой». Каждый объект представляет собой полноценный ключ Redis. Нет необходимости анализировать строки JSON. Недостатки : возможно, медленнее, когда вам нужно получить доступ ко всем / большинству полей объекта. Кроме того, вложенные объекты (объекты внутри объектов) не могут быть легко сохранены.

  3. Сохраните каждый объект в виде строки JSON в хэше Redis.

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

    Это позволяет вам консолидировать немного и использовать только два ключа вместо большого количества ключей. Очевидным недостатком является то, что вы не можете установить TTL (и другие элементы) для каждого пользовательского объекта, поскольку это просто поле в хэше Redis, а не полноценный ключ Redis.

    Преимущества : JSON-анализ выполняется быстро, особенно если вам нужно получить доступ ко многим полям для этого объекта одновременно. Меньше "загрязняющих" пространство имен основного ключа. Недостатки : Примерно такое же использование памяти, как у # 1, когда у вас много объектов. Медленнее, чем # 2, когда вам нужно получить доступ только к одному полю. Вероятно, не считается "хорошей практикой".

  4. Храните каждое свойство каждого объекта в выделенном ключе.

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

    В соответствии со статьей выше, эта опция почти никогда не является предпочтительной (если только свойство Object не должно иметь определенного TTL или чего-то еще).

    Преимущества : Свойства объекта - это полноценные ключи Redis, которые не могут быть излишними для вашего приложения. Недостатки : медленный, использует больше памяти и не считается «лучшей практикой». Множество загрязнений пространства имен основного ключа.

Общая сводка

Вариант 4 обычно не является предпочтительным. Варианты 1 и 2 очень похожи, и оба они довольно распространены. Я предпочитаю вариант 1 (вообще говоря), потому что он позволяет хранить более сложные объекты (с несколькими слоями вложенности и т. Д.). Вариант 3 используется, когда вы действительно хотите не загрязнять пространство имен основного ключа (то есть не хотите там быть много ключей в вашей базе данных, и вы не заботитесь о таких вещах, как TTL, разделение ключей или что-то еще).

Если я что-то не так понял, пожалуйста, оставьте комментарий и разрешите мне пересмотреть ответ, прежде чем понизить голосование. Спасибо! :)

BMiner
источник
4
Для варианта № 2 вы говорите «возможно, медленнее, когда вам нужен доступ ко всем / большинству полей в объекте». Это было проверено?
mikegreiling
4
hmget равен O (n), для n полей get с опцией 1 все равно будет O (1). Теоретически, да, это быстрее.
Аруна Герат
4
Как насчет объединения вариантов 1 и 2 с хешем? Использовать вариант 1 для редко обновляемых данных и вариант 2 для часто обновляемых данных? Скажем, мы храним статьи и храним поля, такие как title, author и url, в строке JSON с общим ключом, например, objи храним поля, такие как views, голосования и избирателей, с отдельными ключами? Таким образом, с помощью одного запроса READ вы получите весь объект и сможете быстро обновить динамические части вашего объекта? Относительно редкие обновления полей в строке JSON можно выполнить, прочитав и записав весь объект обратно в транзакцию.
Арун
2
В соответствии с этим: ( instagram-engineering.tumblr.com/post/12202313862/… ) рекомендуется хранить в нескольких хешах с точки зрения потребления памяти. Итак, после оптимизации arun, мы можем сделать: 1 - сделать несколько хешей, хранящих полезную нагрузку json в виде строк для редко обновляемых данных, и 2 - сделать несколько хешей, хранящих поля json для часто обновляемых данных
Aboelnour
2
В случае варианта 1, почему мы добавляем его в набор? Почему мы не можем просто использовать команду Get и проверить, возвращаем ли мы не nil.
Прагматичный
8

Некоторые дополнения к заданному набору ответов:

Прежде всего, если вы собираетесь эффективно использовать хэширование в Redis, вы должны знать максимальное количество ключей и максимальный размер значений - в противном случае, если они выдают значения из hash-max-ziplist-value или hash-max-ziplist-records, Redis преобразует его в практически обычные пары ключ / значение под капотом. (см. hash-max-ziplist-value, hash-max-ziplist-records). И ломать под капотом из опций хеш-функции очень плохо, потому что каждая обычная пара ключ / значение внутри Redis использует +90 байт на пару.

Это означает, что если вы начнете со второго варианта и случайно выйдете из max-hash-ziplist-value, вы получите +90 байт на КАЖДЫЙ АТРИБУТ, который вы имеете внутри пользовательской модели! (на самом деле не +90, а +70, см. вывод консоли ниже)

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

Для ответа TheHippo комментарии к Варианту 1 вводят в заблуждение:

hgetall / hmset / hmget на помощь, если вам нужны все поля или несколько операций get / set.

За ответ BMiner.

Третий вариант на самом деле очень интересный, для набора данных с max (id) <has-max-ziplist-value это решение имеет сложность O (N), потому что, к удивлению, Reddis хранит небольшие хэши в виде массива как контейнер длины / ключа / значения объекты!

Но много раз хеши содержат всего несколько полей. Когда хэши маленькие, мы можем вместо этого просто закодировать их в структуре данных O (N), как линейный массив с парами ключ-значение с префиксом длины. Поскольку мы делаем это только тогда, когда N мало, время амортизации для команд HGET и HSET по-прежнему равно O (1): хеш будет преобразован в реальную хеш-таблицу, как только число содержащихся в нем элементов будет слишком сильно расти.

Но вы не должны волноваться, вы очень быстро сломаете записи в hash-max-ziplist и приступите к решению № 1.

Второй вариант, скорее всего, пойдет к четвертому решению под капотом, потому что, как говорится в вопросе:

Имейте в виду, что если я использую хеш, длина значения не будет предсказуемой. Они не все короткие, как пример био выше.

И, как вы уже сказали: четвертое решение - самый дорогой +70 байт на каждый атрибут.

Мое предложение, как оптимизировать такой набор данных:

У вас есть два варианта:

  1. Если вы не можете гарантировать максимальный размер некоторых пользовательских атрибутов, тогда вы выбираете первое решение, и если вопрос памяти имеет решающее значение, сожмите пользователя json перед сохранением в redis.

  2. Если вы можете заставить максимальный размер всех атрибутов. Чем вы можете установить hash-max-ziplist-records / value и использовать хэши в виде одного хеша на представление пользователя ИЛИ в качестве оптимизации хеш-памяти из этого раздела руководства Redis: https://redis.io/topics/memory-optimization и сохранить пользователя в виде строки json. В любом случае вы также можете сжать длинные пользовательские атрибуты.

Алексей Лещук
источник