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

Составные структуры с 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

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

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

Недостатки:

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

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

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

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»