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