Education 1.1 Help

Составные структуры с Hashes

В любом проекте присутствуют сложные структуры, которые нужно кэшировать. Например, профиль пользователя. Он состоит из нескольких полей: идентификатор, электронная почта, номер телефона и тд. Возникает вопрос: как лучше хранить такие структуры в Redis?

Первое интуитивное решение — это хранить каждое поле по отдельному ключу.

Два отдельных ключа

Допустим, есть пользователь с ID 56, с электронной почтой user@test.com и номером телефона +7-111-111-11-11. Он будет записан следующим образом:

127.0.0.1:6379> set user:56:email user@test.com OK 127.0.0.1:6379> set user:56:phone '+7-111-111-11-11' OK

Преимущества:

  • интуитивно понятная модель хранения

  • просто получить значение конкретного поля

Недостатки:

  • количество хранимых ключей растет в кратном размере от количества пользователей

  • при обновлении профиля будет происходить N запросов

  • так как каждое поле хранится в своем ключе, обновление информации юзера происходит не атомарно, и несколько параллельных запросов на обновление могут привести к неконсистентному состоянию кэша. Например, пользователь поменял почту на email2, а номер телефона на phone2 во время недоступности сервера, а потом передумал и решил сразу сменить на email3 и phone3. Когда сервер восстановится, к нему придет сразу 2 запроса на обновление. Оба запроса обрабатываются параллельно и каждое поле обновляется атомарно. Такая логика может привести к тому, что в кэше почта будет email2, а телефон phone3 и наоборот. Получается, что состояние профиля в Redis неконсистентно и состоит из 2х разных обновлений. При этом в реляционной базе данных поля будут консистентны: email2 + phone2 или email3 + phone3

Хранить каждое поле в отдельном ключе — не лучшее решение в рамках данной задачи. Попробуем второй вариант с использованием сериализации объекта. Например, перед записью конвертировать объект в JSON строку:

JSON
127.0.0.1:6379> set user:56:profile '{"email":"user@test.com","phone":"+7-111-111-11-11"}' OK

Преимущества:

  • один атомарный запрос на запись/обновление всего профиля

  • количество хранимых ключей равно количеству профилей

Недостатки:

  • чтобы получить значение одного поля, нужно достать всю структуру

  • дополнительная логика сериализации/десериализации со стороны кода бэкенда

Стоит отметить, что в некоторых задачах не требуется получать отдельно поля структуры и тогда вариант с сериализацией можно использовать.

Redis Hashes

К счастью, Redis предоставляет структуру данных для хранения сложных объектов — Hashes. В языках программированию эту структуру так же называют словарем, мапой или ассоциативным массивом.

Используя Hashes, профиль юзера будет храниться в единственном ключе. В любой момент можно получить значение отдельного поля объекта. Также в приложении не будет логики преобразования данных перед записью.

Теперь детально разберем, как работать с Hashes на реальном примере. Представим, что нужно реализовать производительную систему переводов в мультиязычном проекте. Когда клиент открывает платформу, браузер передает язык пользователя на сервер. После этого сервер должен возвращать любые сообщения, которые увидит клиент, на языке браузера.

Формат хранимых переводов будет следующим:

{ "hello": { "en": "hello", "ru": "здравствуйте" }, "bye": { "en": "bye", "ru": "пока" } }

Основной ключ — это идентификатор перевода. Для простоты в данном примере используется английское слово как идентификатор. Внутри словаря лежит структура: язык -> перевод.

Запись

Первым делом запишем несколько переводов в нашу систему с помощью команды hset key field value [field value ...]:

127.0.0.1:6379> hset translates:hello en hello ru привет (integer) 2 127.0.0.1:6379> hset translates:bye en bye ru пока (integer) 2 127.0.0.1:6379> hset translates:name en name ru имя (integer) 2

Команда hset возвращает количество добавленных полей. Если ключа не существовало, то он будет создан.

Похоже, что в переводе слова hello на русский язык есть ошибка. Правильный перевод — это "здравствуйте". Для обновления поля используется та же команда hset:

127.0.0.1:6379> hset translates:hello ru здравствуйте (integer) 0

В ответе вернулся нуль, потому что ничего не добавилось и только изменилось существующее поле.

Чтение

Когда пользователь заходит на стартовую страницу платформы, его нужно поприветствовать на понятном языке. Например, пользователь находится в России, и нужно получить русский перевод приветствия с помощью команды hget key field:

127.0.0.1:6379> hget translates:hello ru "\xd0\xb7\xd0\xb4\xd1\x80\xd0\xb0\xd0\xb2\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb9\xd1\x82\xd0\xb5"

Может показаться, что в ответе вернулась несуразица, однако здесь нет ошибки. Redis сохраняет строки так, как ему передают. Когда в терминале запрашиваются значения, возвращается их UTF-8 интерпретация. Когда эта строка обрабатывается со стороны бэкенда, получается валидный русский текст.

Если необходимо получить всю структуру, в данном примере все переводы, используется команда hgetall key:

127.0.0.1:6379> hgetall translates:name 1) "en" 2) "name" 3) "ru" 4) "\xd0\xb8\xd0\xbc\xd1\x8f"

Удаление

Если какой-то перевод оказался лишним, то его можно удалить командой hdel key field [field ...]:

127.0.0.1:6379> hdel translates:bye ru (integer) 1 127.0.0.1:6379> hgetall translates:bye 1) "en" 2) "bye"

В ответе на команду hdel возвращается количество удаленных полей.

Резюме

  • Хранить сложные объекты можно по-разному. Это напрямую зависит от проекта. Однако чаще всего следует использовать встроенные типы данных Redis для максимальной производительности и функциональности. Несколько преимуществ использования Redis Hashes:

  • один атомарный запрос на запись/обновление всего объекта или отдельных полей

  • количество хранимых ключей равно количеству объектов

  • можно получить/обновить/удалить значение одного поля

  • эффективный формат хранения, абстрагированный от бэкенда

Дополнительные материалы

Вопросы для самопроверки

Какие преимущества имеет использование встроенного типа данных Hashes в Redis в сравнении с ручной сериализацией JSON объектов? (нужно выбрать все корректные ответы)

  • один атомарный запрос на запись/обновление всего объекта или отдельных полей

  • возможность хранить многомерные объекты

  • возможность хранить объекты со свойствами разных типов

  • возможность получать отдельные поля без необходимости чтения всего объекта

Допишите команду добавления электронной почты test@test.comв свойство email по ключу profile:1 в Redis

  • ___ profile:1 ___ ___

Допишите команду получения свойства электронной почты email из ключа profile:1 в Redis

  • ___ profile:1 ___

Допишите команду удаления свойства номера телефона phone из ключа profile:1 в Redis

  • ___ profile:1 ___

Допишите команду получения всех свойств по ключу profile:1 в Redis

  • ___ profile:1

Last modified: 12 June 2024