Redis
2026-02-26 14:59 Diff

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

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

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

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

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

Недостатки:

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

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

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

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

Недостатки:

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

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

Redis Hashes

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

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

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

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

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

Запись

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

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

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

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

Чтение

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

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

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

Удаление

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

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

Резюме

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

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