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

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

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

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

Контекст и таймауты в go-redis

Контекст и таймауты в go-redis — это способ не ждать мёртвый сервер: каждый метод v9 первым аргументом принимает ctx, и его дедлайн ограничивает ожидание команды. Разбираем context.WithTimeout на отдельную операцию, дефолтные ReadTimeout и DialTimeout из Options, судьбу соединения при отмене и шаблон «кэш с таймаутом, промах — в базу».

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

Инцидент, знакомый каждому, кто держал Redis в проде: сетевые качели между инстансами, и половина хендлеров висит по три секунды на rdb.Get, хотя сам кэш-сервер жив. Пользователи видят пустые страницы, дежурный видит всплеск латентности, а виноват — промежуточный роутер, который молча выбросил пакеты. Без ограничения ожидания клиент будет ждать до тех пор, пока операционная система не соскучится по ответу, а это десятки секунд. Контекст решает это штатно: с v9 каждый вызов go-redis принимает ctx первым аргументом, дедлайн и отмена работают на каждом вызове отдельно. В уроке ставим таймауты на кэш-чтения, разбираем, как контекст соотносится с ReadTimeout из Options, и следим за судьбой соединения после отмены — там есть неочевидный поворот.

Зачем go-redis контекст в каждом вызове#

Восьмая версия клиента принимала только адрес и команду; девятая — контекст везде, и это не формальность. ctx управляет всеми тремя ожиданиями вызова: дозвоном до сервера, записью команды в сокет и чтением ответа. Истёк дедлайн — операция вернёт context.DeadlineExceeded, даже если сервер ещё не успел ответить. Вызван cancel — та же история немедленно. Вторая функция контекста — распространение: дедлайн всего запроса ограничивает и вызовы Redis внутри него, отмена отменяет. В HTTP-хендлере вы получаете request-scoped контекст бесплатно и передаёте его дальше — кэш, база и сторонние API оказываются под общим потолком запроса. Основы конструкции разбирались в уроке про контекст в Go — здесь применяем её к Redis. И помните границу: контекст не ускоряет сервер и не отменяет начатую им работу, он только перестаёт ждать.

Кэш-чтение с таймаутом 250 миллисекунд

GO
func cachedUser(ctx context.Context, id int64) (string, error) {
        ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
        defer cancel()

        val, err := rdb.Get(ctx, "cache:user:"+strconv.FormatInt(id, 10)).Result()
        if errors.Is(err, context.DeadlineExceeded) {
                return "", ErrCacheSlow // хендлер уйдёт в базу
        }
        return val, err
}

WithTimeout порождает дочерний контекст с дедлайном через 250 миллисекунд от текущего момента; defer cancel() возвращает ресурсы даже при раннем выходе. Если ответ не успел прийти, Result вернёт context.DeadlineExceeded, и мы отдадим наверх понятную ошибку ErrCacheSlow — хендлер по ней пойдёт в базу, а не повиснет. Затенение имени ctx общепринято: дочерний контекст заменяет родителя внутри функции.

Таймауты go-redis: контекст против Options#

У клиента есть собственные запасные ограничения: DialTimeout по умолчанию пять секунд на установку соединения, ReadTimeout три секунды на ожидание ответа, WriteTimeout повторяет ReadTimeout. Они работают, когда у контекста дедлайна нет. Когда дедлайн есть, действует более короткий из двух: контекст в 250 миллисекунд обрежет вызов раньше, чем трёхсекундный ReadTimeout сработает. Отсюда стратегия: клиентские Options настраиваются один раз под характер сервиса, а контекст — точный инструмент на конкретную операцию. Есть и след, о котором мало кто задумывается: когда ожидание оборвалось по дедлайну, go-redis не вернёт соединение в пул как здоровое, а закроет его — клиент не знает, прилетит ли ответ позже, и рисковать замусориванием не станет. Пара закрытых соединений на сбоях — норма; если соединения рвутся постоянно, значит таймауты слишком жадные, и это уже сигнал к настройке.

Долгие операции: BLPop, подписки и контекст#

Не для каждой операции короткий дедлайн — уместен. BLPop в воркере очереди специально ждёт долго: десятки секунд — это режим работы, а не сбой, и дедлайн туда ставят от внешнего сигнала остановки, а не от секундной жадности. Подписка из урока про Pub/Sub живёт всю жизнь процесса: контекст со WithTimeout в 250 миллисекунд на Subscribe убьёт подписку мгновенно — здесь контекст вообще не ограничивает, а только переносит сигнал остановки из горутины с graceful shutdown. Streams из урока про потоки читаются блокирующим XReadGroup с BLOCK — к ним то же правило. Разумный шаблон для всех долгожителей: контекст без дедлайна, привязанный к корневому; остановка — через cancel при выключении сервиса; внутри циклов чтения — проверка ctx.Err с выходом. Короткие дедлайны — инструмент горячего пути: запросы, чтения кэша, записи сессий, а не фоновые циклы.

Контекст запроса в HTTP-хендлере#

В веб-сервисе правильный контекст уже существует — его создал роутер на весь запрос. В Gin это c.Request.Context(), в chi и net/http — r.Context(): у них общий дедлайн с клиентом, и когда пользователь закрыл вкладку, отменяются и вызовы Redis, что экономит пул от накопления бесполезной работы. Поверх него ставится локальный таймаут на кэш: дедлайн запроса ограничивает сверху, 250-300 миллисекунд на кэш — быстрая ветка снизу. Если кэш не успел, запрос продолжает жить в базу с оставшимся временем. Такая же связка стоит в middleware с таймаутом всего хендлера — её разбор есть в уроке про middleware Gin. Проверяйте связку под нагрузкой, а не на ноутбуке: таймауты, настроенные на холостом Redis, проявляют себя только когда что-то тормозит, а тормозить может и сеть, и сам сервер.

Хендлер Gin: кэш с таймаутом и фоллбек в базу

GO
func userHandler(c *gin.Context) {
        id := c.Param("id")

        ctx, cancel := context.WithTimeout(c.Request.Context(), 300*time.Millisecond)
        defer cancel()

        val, err := rdb.Get(ctx, "cache:user:"+id).Result()
        if err != nil {
                // промах, таймаут или сбой: медленный, но честный ответ из базы
                val = loadUserFromDB(c.Request.Context(), id)
        }
        c.JSON(http.StatusOK, gin.H{"user": val})
}

Родительский контекст — запрос целиком: если клиент ушёл, отмена дойдёт и до Redis, и до базы. Поверх него ставится 300 миллисекунд на кэш; любая ошибка — промах, таймаут или сбой — уводит в базу с контекстом запроса. В бою ветка err != nil заслуживает разбора по видам: redis.Nil и DeadlineExceeded обрабатываются по-разному, этим займётся следующий урок.

Фоновые задачи живут по другим правилам. Горутина, которая чистит просроченные сессии или пушит метрики, не имеет request-контекста — ей нужен context.Background() поверх WithTimeout или тикер с проверкой ctx.Done. Главная ловушка — протащить request-контекст туда, где запрос уже закончился: хендлер отправляет задачу в канал, воркер обрабатывает её с унаследованным ctx, ответ клиенту ушёл, контекст отменён, и половина фоновых операций падает с context canceled. Симптом узнаваемый: «письма отправляются через раз», «инвалидация кэша иногда не срабатывает». Правило простое: у работы после ответа — свой контекст, порождённый от корневого, а не от запроса. С тем, как организовать горутины и их остановку, знакомит курс по продвинутому Go; в уроке про горутины есть базовые паттерны владения. Проверяются ветки таймаутов и в тестах: фейковый Redis из урока про тесты с miniredis стартует за миллисекунды и позволяет прогнать и промах кэша, и сработавший дедлайн без Docker и без ожиданий. И держите в голове ретраи: клиент повторяет только сетевые сбои, а истёкший контекст повторов не заслуживает — это одна из тем следующего урока про ошибки.

Диагностика: медленный Redis или медленная сеть#

Таймауты имеют смысл, когда вы знаете, кто тормозит. SLOWLOG GET на сервере покажет команды, жившие дольше порога: если там пусто, а латентность на клиенте высокая — виновата сеть или очередь на пуле, и таймаутом это не лечится. redis-cli --latency меряет чистые сетевые качели между машиной и сервером; цифры в единицах миллисекунд успокаивают или бьют тревогу быстро. На стороне Go стоит гистограмма длительности вызовов Redis с лейблом по типу операции — Get отдельно от HGetAll, кэш отдельно от записи. С таким набором настройка таймаутов превращается из гадания в арифметику: смотрите p99 операции, ставьте дедлайн с запасом в несколько раз, следите, чтобы фоллбек в базу не оказался дороже ожидания. Без измерений таймаут — только догадка, оформленная кодом, и чаще вредная, чем полезная.

Проверка: что ограничит ожидание

Контекст передан без дедлайна, ReadTimeout в Options не трогали. Что ограничит ожидание rdb.Get?

Проверка: судьба соединения

Дедлайн контекста истёк, пока клиент ждал ответ на Set. Что произойдёт с соединением?

Контекст истёк до вызова

Прочитайте код и выберите, что он выведет:

ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()

time.Sleep(300 * time.Millisecond)

_, err := rdb.Get(ctx, "k").Result()
fmt.Println(errors.Is(err, context.DeadlineExceeded))

Соберите обёртку с таймаутом

Допишите создание контекста с дедлайном, единицу времени и освобождение ресурса.

ctx, cancel := context.(ctx, 250*time.) defer () val, err := rdb.Get(ctx, key).Result()

Ветка таймаута и фоллбек

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

Строки

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

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

Карточки: контекст и таймауты

Термин

ctx в каждом вызове v9

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

Контекст управляет дозвоном, записью и чтением ответа; дедлайн и отмена работают на любой операции. Главное отличие v9 от v8.

1 / 6

Контекст встал на место: дедлайны на операциях, связка с запросом, отдельный контекст у фоновой работы. Дальше логично читать про ошибки go-redis: DeadlineExceeded — лишь одна из веток, и разбор redis.Nil с ретраями продолжает эту тему. Настройки, где живут ReadTimeout и DialTimeout, задаются при создании клиента — подробности в уроке про установку и подключение, а ожидание свободного соединения из пула, PoolTimeout, разбирает урок про пул соединений. Таймауты понадобятся и сессиям: урок про сессии веб-приложения читает профиль на каждый запрос, и без ограничения ожидания один тормозящий Redis уронит весь сайт. Полный план — на странице курса по Redis. И один штрих напоследок: сработавшие дедлайны стоит считать отдельным счётчиком — его график покажет деградацию кэша за часы до того, как её заметят пользователи.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Каждый вызов go-redis v9 принимает контекст: дедлайн ограничивает дозвон, запись и чтение ответа, отмена работает немедленно.
  • Рабочая связка: WithTimeout 250-500 мс на кэш-чтения, промах или дедлайн — фоллбек в базу с контекстом запроса.
  • Дефолты v9: DialTimeout пять секунд, ReadTimeout три; контекстный дедлайн побеждает, если он короче.
  • После отмены соединение закрывается, а не возвращается в пул: клиент не знает, прилетит ли ответ позже.
  • Таймаут клиента не отменяет работу сервера: тяжёлые команды продолжают исполняться, лечит их SLOWLOG, а не увеличение ожидания.
  • Фоновой работе — свой контекст от корневого: request-контекст умирает вместе с ответом и роняет горутину.

Домашнее задание

Оберните кэш-чтение в cachedUser с таймаутом 250 миллисекунд и веткой на DeadlineExceeded. Запустите локальным Go с Redis в Docker, а затем проверьте деградацию: поставьте на Redis команду DEBUG SLEEP 1 в отдельном соединении через redis-cli и убедитесь, что хендлер уходит в базу, а не висит. Добавьте фоновую горутину с контекстом от Background и отдельно попробуйте передать ей request-контекст — увидите context canceled после первого же ответа клиенту.

Личные заметкиПусто
Хранятся только в вашем браузере (Redis · Контекст и таймауты в go-redis)

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

Какой таймаут ставить на кэш-чтения?

Старт с 250-500 миллисекунд: кэш обязан отвечать быстрее базы, иначе смысла в нём нет. Цифру подгоняют под свой SLOWLOG и P99: если нормальный ответ Redis живёт в единицах миллисекунд, полсекунды — щедрый потолок, а не предел мечтаний.

Контекст истёк, а команда всё-таки применилась. Это плохо?

Для Set и Expire — безразлично: команда идемпотентна. Для Incr и LPush — нет: повторный вызов после отмены изменит состояние второй раз. Некоммутируемые операции защищайте идемпотентностью, Lua-скриптом или транзакцией с проверкой.

Можно ли обойтись без контекста и просто увеличить ReadTimeout?

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

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

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

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

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