Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 3 из 25 ~16 мин +20 XP

Типы данных Redis: обзор под задачи Go

Типы данных redis — то, что отличает его от плоского кэша ключ-значение: строки, хэши, списки, множества и ZSET закрывают задачи от профиля пользователя до лидерборда без сторонних сервисов. В этом уроке — карта пяти типов под задачи Go-проекта, по одной команде на каждый и ошибка WRONGTYPE, которая ждёт всех, кто перепутает типы. Копать глубоко не будем: у каждого типа впереди собственный урок.

Содержание урока · 12 разделов

Пересаживаясь с 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члены с числовым scoreZAdd, ZRevRange, ZIncrByлидерборд, тренды, скользящее окно
Streamжурнал записей с IDXAdd, 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 остаётся метрикой.

Кейс из продакшена: 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, и от хаоса в больших схемах. Каждый вызов возвращает команду, из которой достаётся результат; построчные разборы — в уроках про отдельные типы.

Строка с числом: счётчик без мьютексов

GO
rdb.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)

Подберите команды под задачу

Впишите команды: профиль пользователя, уникальные посетители, рейтинг игроков.

// профиль пользователя rdb.(ctx, "user:42", "name", "Аня") // уникальные посетители за день rdb.(ctx, "visitors:2024-06-01", "42") // очки игрока в рейтинге rdb.(ctx, "leaderboard", &redis.Z{Score: 1500, Member: "user:42"})

Соберите поток записи и чтения

Расставьте команды по порядку работы: контекст, клиент, две записи, чтение и вывод.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: типы данных Redis

Термин

String

нажмите, чтобы перевернуть
Определение

Базовый тип: текст, число, JSON, байты. SET/GET для кэша, INCR для атомарных счётчиков. Потолок значения — 512 мегабайт.

1 / 6

Карта типов собрана: строка — дефолт, хэш — объект, список — очередь, множество — уникальность, 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.

Личные заметкиПусто
Хранятся только в вашем браузере (Redis · Типы данных Redis: обзор под задачи Go)

Вопросы и ответы по уроку

Что хранить в строке, а что в хэше?

Если значение читается и пишется целиком — строка: кэш ответа, флаг, счётчик. Если объект имеет поля, которые обновляются по отдельности или читаются выборочно, — хэш. Хэш экономит трафик на частичных чтениях, но для простых значений избыточен.

Можно ли изменить тип существующего ключа?

Нет: тип закреплён за ключом, несовместимая команда вернёт WRONGTYPE. Переход возможен только через удаление ключа DEL и запись заново. На практике проблему предупреждают конвенцией имён: префикс отражает тип и назначение.

Где Redis хранит Streams и почему не везде используют списки?

Streams — отдельный тип журнала: записи имеют ID, хранятся устойчиво к чтению, потребители читают по позициям и объединяются в группы. Список после RPop запись теряет, поэтому для надёжных очередей с подтверждением доставки берут Streams — им посвящён пятнадцатый урок.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

JWT25 уроков~12 чпрактика25 уроков уже доступны

JWT-аутентификация в Go: golang-jwt и bcrypt

Разбираем JWT в Go: устройство токена, golang-jwt v5, подписи HMAC и RSA, refresh-токены, bcrypt, middleware авторизации и лучшие практики.

  • Понимать анатомию JWT и почему payload не зашифрован
  • Выпускать и проверять токены через golang-jwt v5
  • Хешировать пароли bcrypt и сравнивать безопасно
  • Писать middleware аутентификации для chi и Gin