Модуль 1. Redis в Go-проекте: клиент как системный компонент
Цель модуля: решить главный вопрос — зачем вашему сервису Redis
и что вы готовы платить, когда он недоступен. Собрать клиент go-redis
как системный компонент: с осознанными таймаутами, пулом, health-check
и политикой деградации — а не как «ещё одну глобальную переменную».
Перед началом: базовый Go (интерфейсы, контексты, goroutine), Docker. Инструменты:
github.com/redis/go-redis/v9,miniredis(тесты), Docker. Трудоёмкость: ~6–8 ч (теория 3 ч · практика 4 ч).
🔥 Инцидент: «Redis мигнул на 400 мс — а API отходил полчаса»
Ночная перекачка соседнего стенда: Redis перезапустился и был недоступен меньше секунды. Мониторинг Redis показал 400 мс даунтайма. А API-сервис в это время «встал»: p99 вырос с 80 мс до десятков секунд, горутины копились, часть запросов отвалилась по клиентским таймаутам. Отходили полчаса — столько занимало расхлёбывание очереди ожидающих.
Разбор показал: клиент go-redis жили на дефолтах — таймауты в секунды,
несколько ретраев поверх, размер пула «как повезло». Каждый запрос к
недоступному Redis висел долго, занимал горутину и слот пула, а их на каждый
запрос приходилось по два-три. Redis не был причиной деградации — причиной
была конфигурация клиента, о которой никто не думал, «ведь это просто
кеш». В этом модуле собираем клиент как системный компонент: таймауты, пул,
health-check и явную политику — что делать, когда Redis недоступен.
📖 1.1. Карта применений: где Redis оправдан
Redis в Go-сервисе — не «ещё одна база», а набор конкретных механизмов, каждый со своей ценой:
| Применение | Что даёт | Цена ошибки |
|---|---|---|
| Кэш (cache-aside) | Разгрузка БД, мкс-ответы | Пропавшее = перечитаем из БД (дёшево) |
| Распределённые локи | Координация реплик (cron, дедуп) | Зависшая работа или двойная — зависит от контракта |
| Rate limiting | Защита, тарифы, лимиты внешних API | Пропуск лимита при отказе — осознанная цена |
| Идемпотентность (ключи дедупа) | Защита от повторов | Дубль при потере метки (TTL ≥ retention!) |
| Очереди (Streams/Pub-Sub) | Лог, fan-out | Отдельная дисциплина — курс «Redis как брокер» |
Общий принцип цен: чем важнее функция, тем жёстче требования к доступности Redis — и тем осмысленнее политика отказа (§1.3).
Когда Redis не нужен: данные должны переживать отказ (это БД), нужны транзакции между сущностями (это БД), нужна очередь с retention «дни» и миллионами сообщений (это Kafka — курс брокеров).
📖 1.2. Клиент go-redis: конфигурация, которая важнее «hello world»
Дефолты клиента рассчитаны на localhost. Продакшен-конфигурация — это явные ответы на четыре вопроса:
gofunc NewRedis(cfg RedisConfig) *redis.Client { return redis.NewClient(&redis.Options{ Addr: cfg.Addr, Password: cfg.Password, DB: cfg.DB, // 1. Подключение: сколько ждать установки соединения DialTimeout: 2 * time.Second, // 2. Операции: обычные команды должны падать быстро // (кэш-промах дешевле висящего запроса) ReadTimeout: 500 * time.Millisecond, WriteTimeout: 500 * time.Millisecond, // 3. Пул: сколько соединений держать даже в простое // (холодный пул = всплеск латентности при первом трафике) PoolSize: cfg.PoolSize, MinIdleConns: 5, // 4. Очередь команд: клиент копит запросы, когда Redis молчит; // без лимита — рост горутин и памяти при инциденте PoolSize + MaxRetries: cfg.MaxRetries, // 2, с бэкоффом }) }
Ключевые грабли:
- Таймаут по умолчанию — 3 секунды. 3 секунды на кэш-чтение в HTTP хендлере — это бюджет всего запроса; ужимайте до сотен миллисекунд;
- Блокирующие команды требуют отдельного пула.
XREAD BLOCK 0из общего пула «съедает» соединения и душит кэш (детали — курс «Redis как брокер», М1). Правило: blocking-потребители — отдельный клиент; - Клиент создаётся один раз (в composition root приложения), живёт со сервисом и закрывается в shutdown. Не создавайте клиент на запрос.
📖 1.3. Политики отказа: fail-open против fail-close
Это главное архитектурное решение всего курса. Когда Redis недоступен, у каждой функции два честных варианта:
| Функция | Политика | Почему |
|---|---|---|
| Кэш | fail-open: читать из БД без кэша | Сервис продолжает работать, медленнее |
| Rate limiting (входящий) | обычно fail-open: пропускать запросы | Доступность важнее лимита (но решайте по угрозе!) |
| Rate limiting (тарифы/биллинг) | возможно fail-close: отклонять | Иначе раздаёте платное бесплатно |
| Распределённые локи | fail-close (не брать работу) | Двойное выполнение хуже простоя |
| Идемпотентность | fail-close для записи | Дубль списания дороже отказа |
Плохо не ни один из вариантов, а стихийный: когда каждый вызов
redis.Get в глубине кода сам решает, что делать при ошибке. Правильная
форма — политика выражена один раз, в обёртке:
go// CacheGet: fail-open — ошибка Redis неотличима от промаха. func CacheGet(ctx context.Context, rdb *redis.Client, key string) ([]byte, bool) { val, err := rdb.Get(ctx, key).Bytes() if err != nil { if !errors.Is(err, redis.Nil) { metrics.CacheErrors.Inc() // ошибка Redis — считаем отдельно от промаха } return nil, false // для вызывающего: промах } return val, true }
Заметьте разделение redis.Nil (нормальный промах) от ошибки связи —
путаница этих двух случаев выдаёт себя метрикой «кэш всегда промахивается».
📖 1.4. Health-check и наблюдаемость
Redis-зависимости попадают в readiness, но с нюансом: если кэш fail-open, то падение Redis — не повод выводить pod из балансировки (сервис работает!). Разделяйте:
go// Readiness: Redis нужен для fail-close функций (локи, лимиты) func ready(ctx context.Context, rdb *redis.Client) error { ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond) defer cancel() return rdb.Ping(ctx).Err() }
Минимальный набор метрик клиента: операции по типам (get/set/hit/miss),
ошибки (связь vs redis.Nil), латентность (p99 по командам), пул
(redis_client_pool_hits/misses/timeouts от самого клиента). Алерты —
в Модуле 6.
📖 1.5. Тестирование: miniredis против testcontainers
Два уровня, оба нужны:
github.com/alicebob/miniredis/v2— in-memory Redis на Go: мгновенный, без Docker, поддерживает основные команды (включая Lua-скрипты и TTL). Идеален для unit-тестов бизнес-логики поверх Redis;- testcontainers-go — настоящий Redis в Docker: для интеграционных тестов, где важны поведение сети, репликация, eviction.
go// unit: быстро и детерминированно mr := miniredis.RunT(tb) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
Правило: бизнес-код тестируется против интерфейса (см. §1.3 — обёртка
CacheGet), а miniredis/testcontainers — для самих обёрток.
💻 Практика
- Каркас сервиса: конфиг → клиент (таймауты из §1.2) → readiness
/health/readyс Ping → graceful shutdown (rdb.Close()). - Обёртка кэша:
CacheGet/CacheSetс разделениемredis.Nilи ошибок связи + две метрики. - Демонстрация деградации: остановите Redis — сервис жив,
/health/readyкрасный, кэш-запросы промахиваются (fail-open), метрика cache_errors растёт. - Unit-тест обёртки на miniredis: hit / miss / «redis лежит» (mr.Close()) — три кейса.
Артефакт: каркас сервиса + обёртка + три теста.
🧩 Quiz "Проверь себя"
✅ Итоги модуля
| Тема | Главное |
|---|---|
| Карта применений | Кэш/локи/лимиты/идемпотентность/очереди; цена ошибки определяет политику |
| Клиент | Один на приложение; таймауты в сотни мс; MinIdleConns; отдельный пул для blocking |
| Политики отказа | fail-open / fail-close — решаются один раз в обёртке, не в глубине кода |
| Наблюдаемость | Промах ≠ ошибка связи; readiness по fail-close функциям |
| Тесты | miniredis для unit, testcontainers для интеграции |
🔗 Мостик к Модулю 2
Клиент поставлен на рельсы. Дальше — первые два больших применения: кэш (cache-aside без stampede, singleflight, инвалидация) и распределённые локи (SET NX PX + токен, честные границы Redlock) — с ответом, где лок в Redis честен, а где нужен контракт БД.