Содержание урока · 12 разделов
- Типы данных Redis: карта для Go-разработчика
- Пять типов и одна задача на каждый
- Строки: базовый тип для кэша и счётчиков
- Хэши, списки и множества: объекты и очереди
- ZSET: рейтинги, лидерборды и окна времени
- Кейс из продакшена: WRONGTYPE после смены типа
- Как выбрать тип под задачу
- Шпаргалка выбора
- Ответ на частый вопрос: сколько памяти съедает тип
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пересаживаясь с Go на Redis, легко ожидать, что значения будут вести себя как структуры и срезы. Ожидание обманывает: в Redis всё хранится по схеме ключ — тип — значение, и тип закреплён за ключом жёстко. Положили под ключ user:42 строку — это навсегда строковый ключ, пока не удалите его: команда LPUSH по нему вернёт ошибку WRONGTYPE, а не тихо конвертирует значение. Именно поэтому выбор типа — первое решение при проектировании Redis-схемы, и делается оно не по вкусу, а под характер доступа: что читаем чаще, обновляем ли поля по отдельности, нужен ли порядок и сколько должно жить значение. Хорошая новость в том, что типов всего пять с половиной, и каждый закрывает понятный класс задач. Ниже — обзор каждого с командами go-redis, таблица-шпаргалка в середине и правило выбора в конце. Код урока запускается локально: песочнице сайта недоступна сеть, а ваш Redis в Docker из второго урока уже готов принимать команды.
Типы данных Redis: карта для Go-разработчика#
Пять базовых типов — строки, хэши, списки, множества и сортированные множества — плюс шестой, Streams, для журналов событий. Строки — универсальный дефолт: кэш ответа, счётчик, флаг, готовый JSON. Хэши хранят объект как карту поле-значение и позволяют менять одно поле, не трогая остальные. Списки — упорядоченные коллекции с быстрыми операциями по краям, из них собирают очереди задач. Множества хранят уникальные значения без порядка и умеют пересечения и разности. ZSET — то же множество, но у каждого члена есть числовой score, и выборка идёт по диапазону очков: отсюда лидерборды и тренды. Streams — журнал с чтением по позициям и группами потребителей, старший брат списков для серьёзных пайплайнов. Таблица ниже — шпаргалка, к которой стоит возвращаться при проектировании схемы: она отвечает на вопрос «каким типом хранить» за пару секунд.
Пять типов и одна задача на каждый#
| Тип | Что хранит | Команды | Задача в Go-сервисе |
|---|---|---|---|
| String | строка, число, JSON, байты | SET, GET, INCR | кэш ответа, счётчик, флаг |
| Hash | карта поле-значение | HSet, HGet, HGetAll | профиль пользователя, состояние объекта |
| List | упорядоченный список строк | LPush, RPop, BRPop | очередь задач |
| Set | уникальные значения без порядка | SAdd, SIsMember, SInter | уникальные посетители, теги, права |
| ZSET | члены с числовым score | ZAdd, ZRevRange, ZIncrBy | лидерборд, тренды, скользящее окно |
| Stream | журнал записей с ID | XAdd, XRead | лог событий, надёжная очередь |
Строки: базовый тип для кэша и счётчиков#
Строка — единственный тип, который нужен для классического кэша: SET кладёт значение, GET достаёт. Называть её строкой честно только с натяжкой: значение может быть текстом, числом, JSON-документом или произвольными байтами — Redis бинарно-безопасен, а потолок одного значения — 512 мегабайт, до которого почти никто не добирается. Числовые строки получают суперспособность: INCR и DECR атомарно меняют значение, поэтому счётчик просмотров — это строка с числом, и никакой мьютекс не нужен. В go-redis метод Set принимает значение как интерфейс, так что положить можно строку, int, float или слайс байт; структуры сериализуют в JSON руками — этому посвящён отдельный урок про кэш структур. Строки — правильный дефолт: сомневаетесь, каким типом хранить, — кладите строку и усложняйте, только когда упрётесь в задачу.
Хэши, списки и множества: объекты и очереди#
Хэш — карта внутри ключа: HSet пишет поля, HGet читает одно, HGetAll возвращает всё сразу. Профиль пользователя — канонический пример: имя, возраст и статус обновляются по отдельности, хэш экономит трафик и не даёт затереть соседнее поле, как случилось бы с перезаписью целой строки. В Go значения полей — обычные аргументы через запятую или готовая map. Список — упорядоченная коллекция с быстрыми операциями по краям: LPush кладёт в голову, RPop забирает из хвоста, и на этой паре держится простейшая очередь задач — продюсер кладёт, воркер забирает. Блокирующий BRPop усыпляет воркер до появления задачи вместо дежурного опроса пустого списка. Множество — набор уникальных значений без порядка: SAdd добавляет, дубликат тихо игнорируется, SIsMember проверяет наличие за константное время, SInter находит пересечение двух множеств. Уникальные посетители дня, теги статьи, права пользователя — всё это множества. Для сопоставления с Go: hash — map, list — двусторонняя очередь, set — map с пустыми значениями.
ZSET: рейтинги, лидерборды и окна времени#
Сортированное множество — визитная карточка Redis. Каждый член хранится с числовым score, и структура держит порядок по нему постоянно: ZAdd записывает очки, ZIncrBy добавляет к существующим, ZRevRange вытаскивает топ N, ZRank отвечает, на каком месте участник. Лидерборд игры собирается тремя строками кода, и тот же механизм обслуживает тренды: score — число просмотров за час, member — идентификатор статьи. Второе применение — временные окна: score как timestamp превращает ZSET в скользящее окно для rate limiter — добавляем время каждого запроса, чистим устаревшее, считаем размер. В Go-коде запись выглядит как структура redis.Z с полями Score: 1500 и Member: user:42 — score всегда float64, поэтому большие целые идентификаторы в score не кладут — им место в member, а score остаётся метрикой.
Самая обидная ошибка первых недель: ключ создан через SET, а вы зовёте LPUSH — сервер отвечает WRONGTYPE Operation not permitted. Redis не конвертирует типы: за ключом закреплена одна структура, и чтобы переиспользовать имя, ключ надо удалить через DEL. Лечится конвенцией имён: префикс отражает назначение — counter:, queue:, board:, user: — и коллизии не случается. В go-redis ошибка приходит обычным err из Result, без паник и особых сценариев.
Кейс из продакшена: WRONGTYPE после смены типа#
История из реального проекта: корзину хранили строкой с JSON, в релизе переехали на хэш — удобнее менять поля по отдельности. Новый код записывает HSet, а старый воркер отложенных заказов по-прежнему зовёт GET по тому же ключу — и тут тонкость: GET по хэшу сервер вернёт ошибку WRONGTYPE ровно так же, как LPUSH по строке. Воркер стал падать на каждом заказе, очередь копилась, пользователи не получали подтверждений — классический инцидент из-за смены типа под живым ключом. Разбор дал два урока. Первый: тип ключа — часть контракта, и менять его можно только с миграцией, которая переименовывает ключи или переливает данные под новым именем. Второй: конвенция префиксов в имени — cart:, cart:v2: — дешёвый способ развести версии схемы и развести старый и новый код. WRONGTYPE в логах после релиза ищите сразу: это почти всегда переходный период смены схемы.
Как выбрать тип под задачу#
Правило выбора стоит проговорить как чек-лист. Значение пишется и читается целиком — берите строку. Объект с полями, которые меняются по отдельности, — хэш. Важен порядок обработки, задачи приходят и уходят — список. Нужны уникальность и проверка наличия — множество. Нужен порядок по числовому критерию, топы и диапазоны — ZSET. Нужен журнал, который переживает чтение и распределение между воркерами, — Streams. Если задача попадает в два пункта сразу, начинайте с более простого типа: хэш вместо строки окупается, только когда полей много и они живут своей жизнью, а Streams вместо списка — когда сообщениям нужна подтверждаемая доставка. Простые типы дешевле обслуживать, а миграцию между типами в Redis не завезли.
Шпаргалка выбора#
- Кэш целиком, флаг, число — строка: SET, GET, INCR.
- Объект с независимыми полями — хэш: HSet, HGet, HGetAll.
- Очередь заданий — список: LPush, RPop, BRPop.
- Уникальность и пересечения — множество: SAdd, SIsMember, SInter.
- Рейтинг, топ, окно по времени — ZSET: ZAdd, ZRevRange.
- Журнал событий с группами потребителей — Streams: XAdd, XRead.
Ответ на частый вопрос: сколько памяти съедает тип#
Вопрос, который всплывает на планёрках по лимитам памяти: зависит ли расход от типа. Зависит, и заметно. Каждый ключ в Redis — это не только значение, но и служебные структуры вокруг него, поэтому миллион мелких строк весит ощутимо больше, чем сумма их содержимого. Мелкий хэш, список или множество сервер хранит компактной кодировкой вроде listpack — несколько полей в одном ключе обходятся дешевле, чем несколько отдельных ключей-строк. Но у компактной кодировки есть порог по числу элементов и по размеру значений: после него структура переключается на общую кодировку, и компактность уходит. Практических следствия два. Объект с десятком полей выгоднее держать хэшем, чем рассыпать по ключам user:42:name, user:42:age. А список на миллион записей — это не компактность, а нагрузка на память и на команды по краям; для таких объёмов существует Streams и посвящённый им урок.
Четыре типа — четыре команды
GO// профиль пользователя целиком
rdb.HSet(ctx, "user:42", "name", "Аня", "age", 30)
// очередь: задача в конец списка
rdb.RPush(ctx, "jobs", "send-email:42")
// уникальные посетители за день
rdb.SAdd(ctx, "visitors:2024-06-01", "42")
// очки игрока в рейтинге
rdb.ZAdd(ctx, "leaderboard", &redis.Z{Score: 1500, Member: "user:42"})Четыре команды — четыре типа: хэш профиля, очередь задач, множество онлайна и рейтинг. Имена ключей построены по конвенции сущность:идентификатор — она убережёт и от WRONGTYPE, и от хаоса в больших схемах. Каждый вызов возвращает команду, из которой достаётся результат; построчные разборы — в уроках про отдельные типы.
Строка с числом: счётчик без мьютексов
GOrdb.Set(ctx, "views:article:7", 0, 0)
rdb.Incr(ctx, "views:article:7")
rdb.Incr(ctx, "views:article:7")
n, _ := rdb.Get(ctx, "views:article:7").Result()
fmt.Println(n) // 2Строки с числами поддерживают атомарную арифметику: Set положил ноль, два вызова Incr подняли счётчик до двух. Никаких гонок: команды исполняются по очереди в одном потоке сервера. Просмотры, лайки, лимиты — это ровно этот паттерн; полный разбор INCR и его применений — в уроке про счётчики.
Проверка: лидерборд
Какой тип выбрать для топ-100 игроков по очкам?
Проверка: WRONGTYPE
Ключ создан через SET, код зовёт LPUSH по нему же. Что вернётся?
Что вернёт HGet?
Прочитайте код и выберите, что он выведет:
rdb.HSet(ctx, "user:42", "name", "Аня", "age", 30)
rdb.HSet(ctx, "user:42", "age", 31)
age, err := rdb.HGet(ctx, "user:42", "age").Result()
fmt.Println(age, err)Подберите команды под задачу
Впишите команды: профиль пользователя, уникальные посетители, рейтинг игроков.
Соберите поток записи и чтения
Расставьте команды по порядку работы: контекст, клиент, две записи, чтение и вывод.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: типы данных Redis
Карта типов собрана: строка — дефолт, хэш — объект, список — очередь, множество — уникальность, ZSET — порядок по очкам. Следующий урок — строки в деле: GET, SET и первые ошибки, где соберём первый настоящий кэш и отличим redis.Nil от аварии. Дальше каждый тип получит свой урок — от хэшей до лидербордов на ZSET, а полный план — на странице курса. Приятная новость для тех, кто работает с базой через GORM: типы Redis идеально ложатся на кэширование моделей, этим займёмся в шестом уроке. Код запускается локально — Docker с Redis из второго урока всё ещё ждёт ваши команды.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Схема Redis — ключ, тип, значение: тип закреплён за ключом до удаления.
- Пять базовых типов плюс Streams; строка — универсальный дефолт и правильная первая ставка.
- Хэш — объект с независимыми полями, список — очередь, множество — уникальность, ZSET — порядок по score.
- INCR делает числовую строку атомарным счётчиком без мьютексов.
- WRONGTYPE — сигнал несовместимой команды: лечится конвенцией имён ключей.
- score в ZSET — float64: идентификаторы живут в member, а не в score.
- Проверять типы удобно на живом Redis из Docker: песочница сети не умеет.
Домашнее задание
Поднимите Redis из второго урока и воспроизведите таблицу типов командами go-redis: HSet по user:42, RPush в jobs, SAdd в visitors, ZAdd в leaderboard. Затем специально вызовите LPUSH по строковому ключу и рассмотрите ошибку WRONGTYPE в err. Песочница для этого не подойдёт — нужен локальный Go и запущенный Redis.
Личные заметкиПусто
Вопросы и ответы по уроку
Что хранить в строке, а что в хэше?
Если значение читается и пишется целиком — строка: кэш ответа, флаг, счётчик. Если объект имеет поля, которые обновляются по отдельности или читаются выборочно, — хэш. Хэш экономит трафик на частичных чтениях, но для простых значений избыточен.
Можно ли изменить тип существующего ключа?
Нет: тип закреплён за ключом, несовместимая команда вернёт WRONGTYPE. Переход возможен только через удаление ключа DEL и запись заново. На практике проблему предупреждают конвенцией имён: префикс отражает тип и назначение.
Где Redis хранит Streams и почему не везде используют списки?
Streams — отдельный тип журнала: записи имеют ID, хранятся устойчиво к чтению, потребители читают по позициям и объединяются в группы. Список после RPop запись теряет, поэтому для надёжных очередей с подтверждением доставки берут Streams — им посвящён пятнадцатый урок.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
JWT-аутентификация в Go: golang-jwt и bcrypt
Разбираем JWT в Go: устройство токена, golang-jwt v5, подписи HMAC и RSA, refresh-токены, bcrypt, middleware авторизации и лучшие практики.
- Понимать анатомию JWT и почему payload не зашифрован
- Выпускать и проверять токены через golang-jwt v5
- Хешировать пароли bcrypt и сравнивать безопасно
- Писать middleware аутентификации для chi и Gin