0 added
0 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>В любом проекте присутствуют сложные структуры, которые нужно кэшировать. Например, профиль пользователя. Он состоит из нескольких полей: идентификатор, электронная почта, номер телефона и тд. Возникает вопрос: как лучше хранить такие структуры в Redis?</p>
1
<p>В любом проекте присутствуют сложные структуры, которые нужно кэшировать. Например, профиль пользователя. Он состоит из нескольких полей: идентификатор, электронная почта, номер телефона и тд. Возникает вопрос: как лучше хранить такие структуры в Redis?</p>
2
<p>Первое интуитивное решение - это хранить каждое поле по отдельному ключу.</p>
2
<p>Первое интуитивное решение - это хранить каждое поле по отдельному ключу.</p>
3
<p>Допустим, есть пользователь с ID<em>56</em>, с электронной почтой<em><a>user@test.com</a></em>и номером телефона<em>+7-111-111-11-11</em>. Он будет записан следующим образом:</p>
3
<p>Допустим, есть пользователь с ID<em>56</em>, с электронной почтой<em><a>user@test.com</a></em>и номером телефона<em>+7-111-111-11-11</em>. Он будет записан следующим образом:</p>
4
<p>Преимущества:</p>
4
<p>Преимущества:</p>
5
<ul><li>интуитивно понятная модель хранения</li>
5
<ul><li>интуитивно понятная модель хранения</li>
6
<li>просто получить значение конкретного поля</li>
6
<li>просто получить значение конкретного поля</li>
7
</ul><p>Недостатки:</p>
7
</ul><p>Недостатки:</p>
8
<ul><li>количество хранимых ключей растет в кратном размере от количества пользователей</li>
8
<ul><li>количество хранимых ключей растет в кратном размере от количества пользователей</li>
9
<li>при обновлении профиля будет происходить N запросов</li>
9
<li>при обновлении профиля будет происходить N запросов</li>
10
<li>так как каждое поле хранится в своем ключе, обновление информации юзера происходит не атомарно, и несколько параллельных запросов на обновление могут привести к неконсистентному состоянию кэша. Например, пользователь поменял почту на<em>email2</em>, а номер телефона на<em>phone2</em>во время недоступности сервера, а потом передумал и решил сразу сменить на<em>email3</em>и<em>phone3</em>. Когда сервер восстановится, к нему придет сразу 2 запроса на обновление. Оба запроса обрабатываются параллельно и каждое поле обновляется атомарно. Такая логика может привести к тому, что в кэше почта будет<em>email2</em>, а телефон phone3 и наоборот. Получается, что состояние профиля в Redis неконсистентно и состоит из 2х разных обновлений. При этом в реляционной базе данных поля будут консистентны:<em>email2</em>+<em>phone2</em>или<em>email3</em>+<em>phone3</em></li>
10
<li>так как каждое поле хранится в своем ключе, обновление информации юзера происходит не атомарно, и несколько параллельных запросов на обновление могут привести к неконсистентному состоянию кэша. Например, пользователь поменял почту на<em>email2</em>, а номер телефона на<em>phone2</em>во время недоступности сервера, а потом передумал и решил сразу сменить на<em>email3</em>и<em>phone3</em>. Когда сервер восстановится, к нему придет сразу 2 запроса на обновление. Оба запроса обрабатываются параллельно и каждое поле обновляется атомарно. Такая логика может привести к тому, что в кэше почта будет<em>email2</em>, а телефон phone3 и наоборот. Получается, что состояние профиля в Redis неконсистентно и состоит из 2х разных обновлений. При этом в реляционной базе данных поля будут консистентны:<em>email2</em>+<em>phone2</em>или<em>email3</em>+<em>phone3</em></li>
11
</ul><p>Хранить каждое поле в отдельном ключе - не лучшее решение в рамках данной задачи. Попробуем второй вариант с использованием сериализации объекта. Например, перед записью конвертировать объект в JSON строку:</p>
11
</ul><p>Хранить каждое поле в отдельном ключе - не лучшее решение в рамках данной задачи. Попробуем второй вариант с использованием сериализации объекта. Например, перед записью конвертировать объект в JSON строку:</p>
12
<p>Преимущества:</p>
12
<p>Преимущества:</p>
13
<ul><li>один атомарный запрос на запись/обновление всего профиля</li>
13
<ul><li>один атомарный запрос на запись/обновление всего профиля</li>
14
<li>количество хранимых ключей равно количеству профилей</li>
14
<li>количество хранимых ключей равно количеству профилей</li>
15
</ul><p>Недостатки:</p>
15
</ul><p>Недостатки:</p>
16
<ul><li>Без модуля RedisJSON нельзя получить значение одного поля, нужно достать всю структуру</li>
16
<ul><li>Без модуля RedisJSON нельзя получить значение одного поля, нужно достать всю структуру</li>
17
<li>дополнительная логика сериализации/десериализации со стороны кода бэкенда</li>
17
<li>дополнительная логика сериализации/десериализации со стороны кода бэкенда</li>
18
</ul><p>Стоит отметить, что в некоторых задачах не требуется получать отдельно поля структуры и тогда вариант с сериализацией можно использовать.</p>
18
</ul><p>Стоит отметить, что в некоторых задачах не требуется получать отдельно поля структуры и тогда вариант с сериализацией можно использовать.</p>
19
<h2>Redis Hashes</h2>
19
<h2>Redis Hashes</h2>
20
<p>К счастью, Redis предоставляет структуру данных для хранения сложных объектов - Hashes. В языках программирования эту структуру так же называют словарем, мапой или ассоциативным массивом.</p>
20
<p>К счастью, Redis предоставляет структуру данных для хранения сложных объектов - Hashes. В языках программирования эту структуру так же называют словарем, мапой или ассоциативным массивом.</p>
21
<p>Используя Hashes, профиль юзера будет храниться в единственном ключе. В любой момент можно получить значение отдельного поля объекта. Также в приложении не будет логики преобразования данных перед записью.</p>
21
<p>Используя Hashes, профиль юзера будет храниться в единственном ключе. В любой момент можно получить значение отдельного поля объекта. Также в приложении не будет логики преобразования данных перед записью.</p>
22
<p>Теперь детально разберем, как работать с Hashes на реальном примере. Представим, что нужно реализовать производительную систему переводов в мультиязычном проекте. Когда клиент открывает платформу, браузер передает язык пользователя на сервер. После этого сервер должен возвращать любые сообщения, которые увидит клиент, на языке браузера.</p>
22
<p>Теперь детально разберем, как работать с Hashes на реальном примере. Представим, что нужно реализовать производительную систему переводов в мультиязычном проекте. Когда клиент открывает платформу, браузер передает язык пользователя на сервер. После этого сервер должен возвращать любые сообщения, которые увидит клиент, на языке браузера.</p>
23
<p>Формат хранимых переводов будет следующим:</p>
23
<p>Формат хранимых переводов будет следующим:</p>
24
<p>Основной ключ - это идентификатор перевода. Для простоты в данном примере используется английское слово как идентификатор. Внутри словаря лежит структура: язык -> перевод.</p>
24
<p>Основной ключ - это идентификатор перевода. Для простоты в данном примере используется английское слово как идентификатор. Внутри словаря лежит структура: язык -> перевод.</p>
25
<h3>Запись</h3>
25
<h3>Запись</h3>
26
<p>Первым делом запишем несколько переводов в нашу систему с помощью команды hset key field value [field value ...]:</p>
26
<p>Первым делом запишем несколько переводов в нашу систему с помощью команды hset key field value [field value ...]:</p>
27
<p>Команда hset возвращает количество добавленных полей. Если ключа не существовало, то он будет создан.</p>
27
<p>Команда hset возвращает количество добавленных полей. Если ключа не существовало, то он будет создан.</p>
28
<p>Похоже, что в переводе слова<em>hello</em>на русский язык есть ошибка. Правильный перевод - это "здравствуйте". Для обновления поля используется та же команда hset:</p>
28
<p>Похоже, что в переводе слова<em>hello</em>на русский язык есть ошибка. Правильный перевод - это "здравствуйте". Для обновления поля используется та же команда hset:</p>
29
<p>В ответе вернулся нуль, потому что ничего не добавилось и только изменилось существующее поле.</p>
29
<p>В ответе вернулся нуль, потому что ничего не добавилось и только изменилось существующее поле.</p>
30
<h3>Чтение</h3>
30
<h3>Чтение</h3>
31
<p>Когда пользователь заходит на стартовую страницу платформы, его нужно поприветствовать на понятном языке. Например, пользователь находится в России, и нужно получить русский перевод приветствия с помощью команды hget key field:</p>
31
<p>Когда пользователь заходит на стартовую страницу платформы, его нужно поприветствовать на понятном языке. Например, пользователь находится в России, и нужно получить русский перевод приветствия с помощью команды hget key field:</p>
32
<p>Может показаться, что в ответе вернулась несуразица, однако здесь нет ошибки. Redis сохраняет строки так, как ему передают. Когда в терминале запрашиваются значения, возвращается их UTF-8 интерпретация. Когда эта строка обрабатывается со стороны бэкенда, получается валидный русский текст.</p>
32
<p>Может показаться, что в ответе вернулась несуразица, однако здесь нет ошибки. Redis сохраняет строки так, как ему передают. Когда в терминале запрашиваются значения, возвращается их UTF-8 интерпретация. Когда эта строка обрабатывается со стороны бэкенда, получается валидный русский текст.</p>
33
<p>Если необходимо получить всю структуру, в данном примере все переводы, используется команда hgetall key:</p>
33
<p>Если необходимо получить всю структуру, в данном примере все переводы, используется команда hgetall key:</p>
34
<h3>Удаление</h3>
34
<h3>Удаление</h3>
35
<p>Если какой-то перевод оказался лишним, то его можно удалить командой hdel key field [field ...]:</p>
35
<p>Если какой-то перевод оказался лишним, то его можно удалить командой hdel key field [field ...]:</p>
36
<p>В ответе на команду hdel возвращается количество удаленных полей.</p>
36
<p>В ответе на команду hdel возвращается количество удаленных полей.</p>
37
<h2>Резюме</h2>
37
<h2>Резюме</h2>
38
<p>Хранить сложные объекты можно по-разному. Это напрямую зависит от проекта. Однако чаще всего следует использовать встроенные типы данных Redis для максимальной производительности и функциональности. Несколько преимуществ использования Redis Hashes:</p>
38
<p>Хранить сложные объекты можно по-разному. Это напрямую зависит от проекта. Однако чаще всего следует использовать встроенные типы данных Redis для максимальной производительности и функциональности. Несколько преимуществ использования Redis Hashes:</p>
39
<ul><li>один атомарный запрос на запись/обновление всего объекта или отдельных полей</li>
39
<ul><li>один атомарный запрос на запись/обновление всего объекта или отдельных полей</li>
40
<li>количество хранимых ключей равно количеству объектов</li>
40
<li>количество хранимых ключей равно количеству объектов</li>
41
<li>можно получить/обновить/удалить значение одного поля</li>
41
<li>можно получить/обновить/удалить значение одного поля</li>
42
<li>эффективный формат хранения, абстрагированный от бэкенда</li>
42
<li>эффективный формат хранения, абстрагированный от бэкенда</li>
43
</ul>
43
</ul>