HTML Diff
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>Основной ключ - это идентификатор перевода. Для простоты в данном примере используется английское слово как идентификатор. Внутри словаря лежит структура: язык -&gt; перевод.</p>
24 <p>Основной ключ - это идентификатор перевода. Для простоты в данном примере используется английское слово как идентификатор. Внутри словаря лежит структура: язык -&gt; перевод.</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>