Зарегистрируйтесь, чтобы продолжить обучение

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

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

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

two_separate_keys

Допустим, есть пользователь с 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 строку:

key_json

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

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

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

Недостатки:

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

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

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:

  • один атомарный запрос на запись/обновление всего объекта или отдельных полей
  • количество хранимых ключей равно количеству объектов
  • можно получить/обновить/удалить значение одного поля
  • эффективный формат хранения, абстрагированный от бэкенда

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

  1. Redis HSET command
  2. Redis HGET command
  3. Redis HGETALL command
  4. Redis HDEL command

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff