Составные структуры с Hashes
В любом проекте присутствуют сложные структуры, которые нужно кэшировать. Например, профиль пользователя. Он состоит из нескольких полей: идентификатор, электронная почта, номер телефона и тд. Возникает вопрос: как лучше хранить такие структуры в 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 строку:
Преимущества:
один атомарный запрос на запись/обновление всего профиля
количество хранимых ключей равно количеству профилей
Недостатки:
чтобы получить значение одного поля, нужно достать всю структуру
дополнительная логика сериализации/десериализации со стороны кода бэкенда
Стоит отметить, что в некоторых задачах не требуется получать отдельно поля структуры и тогда вариант с сериализацией можно использовать.
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:
один атомарный запрос на запись/обновление всего объекта или отдельных полей
количество хранимых ключей равно количеству объектов
можно получить/обновить/удалить значение одного поля
эффективный формат хранения, абстрагированный от бэкенда
Дополнительные материалы
Вопросы для самопроверки
Какие преимущества имеет использование встроенного типа данных 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