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

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

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

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

Пайплайны: много команд за один рейс

Пайплайн в go-redis — способ отправить серию команд Redis за один сетевой рейс вместо отдельного рейса на каждую команду. Типичная ситуация: прогрев кэша на тысячу ключей или десяток инкрементов в одном хендлере — каждая операция быстрая, но суммарно запросы ждут не сервер, а сеть. Соберём пайплайны через rdb.Pipelined, вытащим результаты отдельных команд, отмерим размер батча и проговорим, чем пайплайн отличается от транзакции из следующего урока.

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

Сценарий, где пайплайн спасает: сервис прогревает кэш после деплоя. Товаров тысяча, полей у товара пять — это пять тысяч команд Set, и по одной за рейс они превращаются в пять тысяч сетевых поездок. При латентности сети в полмиллисекунды даже параллельными горутинами прогрев размажется на секунды и забьёт пул соединений — его устройство подробно разбирает урок про пул соединений. Пайплайн сжимает эти пять тысяч рейсов в пятьсот батчей по десять команд, а при желании и в пятьдесят по сотне: сеть перестаёт быть узким местом, потому что один пакет уходит и один блок ответов возвращается. Та же механика работает в обратную сторону: хендлер, который читает профиль, настройки и счётчик тремя вызовами rdb.Get, платит тремя рейсами за то, что могло быть одним.

Пайплайн go-redis: что происходит на проводе#

Механика простая, но её полезно держать в голове. Без пайплайна каждая команда проходит полный цикл: клиент пишет запрос в сокет, ядро гонит пакет через сеть, Redis отвечает, ответ снова едет назад. Четыре переключения на команду, и почти всё это время процессор простаивает. С пайплайном клиент буферизует команды в памяти, отправляет их одним куском и ждёт один блок ответов: Redis выполняет команды по очереди — он однопоточный — и складывает ответы в том же порядке. go-redis возвращает срез объектов redis.Cmder, где результат каждой команды лежит на своём месте. Два свойства важно не перепутать. Первое: пайплайн ускоряет сеть, но не даёт атомарности — между командами вашего батча сервер спокойно выполняет команды других клиентов. Второе: порядок ответов строго совпадает с порядком запросов, поэтому результаты не подписаны именами ключей и читаются по позиции. Если нужна атомарность, это уже MULTI/EXEC — следующий урок курса.

rdb.Pipelined: планируем команды замыканием

GO
var hits *redis.IntCmd

cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
        hits = pipe.Incr(ctx, "stats:views:42")
        pipe.Expire(ctx, "stats:views:42", 24*time.Hour)
        pipe.Set(ctx, "cache:user:42", "annie", time.Hour)
        return nil
})
if err != nil {
        return err
}

fmt.Println(hits.Val()) // результат конкретной команды
fmt.Println(len(cmds))  // 3: каждая команда — свой Cmder

Pipelined принимает замыкание, в котором вы только планируете команды: pipe.Incr и pipe.Set копируют запросы в буфер и возвращают типизированные команды — реальная отправка случится после выхода из замыкания. Результат Incr достаётся через hits.Val() после выполнения батча, Err() отдаёт ошибку конкретной команды. Переменную под команду объявляют снаружи замыкания — стандартный приём go-redis. Для запуска нужен локальный Go и Redis в Docker: песочница сайта сетевые соединения не открывает.

Результаты команд: Cmder и приведение типов#

Каждый элемент среза cmds — интерфейс redis.Cmder, и у него два слоя данных: общий Err() и конкретное значение через приведение к точному типу. StatusCmd у Set и Expire, IntCmd у Incr и Del, StringCmd у Get, SliceCmd у MGet. Приведение обычное, через type assertion: cmds[2].(*redis.StringCmd), дальше cmd.Val(). Когда команд десятки, чаще перебирают срез по индексам и приводят по позиции — главное держать порядок планирования и порядок чтения согласованным, потому что порядок ответов совпадает с порядком запросов один в один. Отдельно про ошибки: если одна команда упала, остальные всё равно выполнятся, а Pipelined вернёт первую ошибку из результатов. Проверять её нужно у каждой значимой команды, а не только на общем err — иначе промах кэша redis.Nil внутри батча отменит вам весь прогрев, хотя физически ничего не сломалось.

Прогрев кэша батчами по пятьсот

GO
func warmCache(ctx context.Context, items []Item) error {
        const batchSize = 500

        for start := 0; start < len(items); start += batchSize {
                end := min(start+batchSize, len(items))

                _, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
                        for _, item := range items[start:end] {
                                key := "cache:item:" + strconv.Itoa(item.ID)
                                pipe.Set(ctx, key, item.JSON, 6*time.Hour)
                        }
                        return nil
                })
                if err != nil {
                        return err
                }
        }
        return nil
}

Пачка в пятьсот команд — рабочая точка для прогрева: батч занимает десятки килобайт в буферах клиента и сервера, а сбой сети между пачками не теряет весь прогрев целиком. Константу подбирают по размеру значения: при килобайтовых JSON уменьшайте до сотни, при коротких флагах можно и тысячу. Цикл внутри замыкания — нормальная практика: планирование ничего не отправляет, аллокации остаются на стороне клиента.

Сколько команд класть в один батч#

Верхнего предела в API нет, но в реальности он есть, и не в клиенте. Сервер копит ответы, пока клиент их читает: батч на миллион команд — это мегабайты выходного буфера Redis, и при превышении лимита client-output-buffer-limit сервер разорвёт соединение — вы потеряете всю пачку и получите переподключение. Память буферизуется и на клиенте: go-redis складывает команды в срез, и миллион команд перед отправкой — лишние аллокации в вашем процессе. Практичная граница — сотни команд на батч: сетевой выигрыш уже почти максимальный, потому что рейс один, а риски буферов исчезают. Если объём больше — режьте на пачки циклом, как в примере выше. Отдельный случай — интерактивные хендлеры: там пайплайн из двух-пяти команд уже даёт заметный выигрыш, потому что латентность сети доминирует над временем исполнения самих команд.

Батчирование и его соседи#

ПодходРейсов по сетиАтомарностьКогда выбирать
Команда за командойNНетОдна-две операции на запрос
Пайплайн (Pipelined)1НетМного независимых команд: прогрев, счётчики, мультиключевое чтение
TxPipeline (MULTI/EXEC)1ДаГруппа, которая обязана примениться целиком
Lua-скрипт (EVAL)1ДаЧтение и ветвление с записью в одной атомарной единице

Пайплайн и пул соединений: кто кого держит#

Пайплайн занимает одно соединение из пула на всё время батча: от первой планируемой команды до последнего прочитанного ответа. Пока батч летит, другие горутины ждут свободный коннект — и это нормально, пока батч короткий. Десятитысячный батч на медленной сети держит соединение секунды, и вся остальная очередь пула в это время стоит: латентность обычных запросов скачет, хотя сервер Redis здоров. Отсюда правило: массовые операции уводите в фоновые горутины с собственным темпом, а в хендлерах держите батчи из единиц команд. Второй нюанс — ретраи: если соединение оборвалось посреди батча, go-redis повторит всю пачку целиком, поэтому команды внутри должны переносить повторное выполнение. Set и Expire переносят спокойно, Incr — нет: после повтора счётчик уедет вверх дважды. Некоммутируемые операции в пайплайнах лучше заменять Set-ом абсолютного значения или Lua-скриптом.

Соседняя развилка, которую стоит видеть: родные мультиключевые команды. MGet читает десяток строковых ключей одним вызовом, MSet пишет их же — и часто этого хватает без всякого пайплайна. Разница в гибкости: MGet отдаёт только строки и один тип команды, а пайплайн смешивает что угодно — Get по профилю, HGetAll по настройкам, ZScore по рейтингу, плюс Expire и Del в том же батче. Практическое правило: однотипное чтение строк — MGet, разнородные операции или запись с TTL — пайплайн. Есть и цена у MGet: он бьёт по куче ключей одной командой, и в кластере Redis ключи из разных слотов одной такой командой не обслужить, а пайплайн рассыпается по нодам без проблем. Пока вы на одиночном сервере, разницы в ограничениях нет — выбирайте по читаемости кода.

Где пайплайны живут в реальных сервисах. Прогрев кэша после деплоя — тот самый warmCache. Метрики и счётчики: раз в секунду сервис отправляет пачку Incr вместо сотни отдельных вызовов, и нагрузка на пул падает в разы. Мультиключевое чтение в хендлере: профиль, настройки и лимиты пользователя лежат в трёх ключах — один пайплайн вместо трёх rdb.Get сокращает латентность ответа на два рейса, что на удалённом Redis означает единицы миллисекунд с каждого запроса. Похоже на проблему N+1 в базах: там её решает preload из курса по GORM, здесь — батчирование команд. Граница та же, что и с базой: если команды зависят друг от друга, батчирование не поможет, нужна другая модель. И честная оговорка: все примеры урока требуют локального Go и Redis в Docker — сетевые соединения песочница не открывает, а разницу в латентности видно только на живом сервере.

Проверка: рейсы по сети

Хендлер выполняет 40 команд через один rdb.Pipelined. Сколько сетевых рейсов между клиентом и Redis это займёт, не считая дозвона?

Проверка: упала одна команда из пяти

В пайплайне из пяти команд одна упала с WRONGTYPE, остальные корректны. Что произойдёт?

Что вернёт Pipelined при промахе?

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

cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
        pipe.Set(ctx, "flag", "on", 0)
        pipe.Get(ctx, "no-such-key")
        return nil
})

fmt.Println(cmds[0].Err() == nil, err == redis.Nil)

Соберите пайплайн со счётчиком

Допишите запуск пайплайна, инкремент внутри него и чтение результата после выполнения.

var counter *redis.IntCmd _, err := rdb.(ctx, func(pipe redis.Pipeliner) error { counter = pipe.(ctx, "stats:hits") pipe.Expire(ctx, "stats:hits", time.Hour) return nil }) if err != nil { return err } fmt.Println(counter.()) // значение после выполнения батча

Ручной вариант: Pipeline и Exec

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

Строки

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

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

Карточки: пайплайны

Термин

rdb.Pipelined

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

Выполняет замыкание с планированием, отправляет буфер одним рейсом и возвращает срез Cmder. Одна сетевая поездка на любое число команд.

1 / 5

Пайплайн встал на место: сетевые рейсы сжаты, результаты вытащены, батчи отмерены. Дальше по курсу две соседние темы: урок про транзакции MULTI/EXEC добавляет к сжатию сети атомарность — тот же буфер команд заворачивается в MULTI и EXEC, а пайплайн превращается в TxPipeline. Если после пайплайна часть команд всё равно падает с таймаутами — загляните в урок про ошибки go-redis, там разобраны ретраи и redis.Nil. Много мелких команд на запрос живёт и в сессиях: урок про сессии веб-приложения покажет, где батчирование читает профиль и настройки одним рейсом. Полный план остальных тем — на странице курса по Redis. Домашнее задание ниже прогреет кэш двумя размерами батча — замерьте время в терминале, разница между рейсами видна невооружённым глазом.

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

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

Вывод

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

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

  • Пайплайн буферизует команды и отправляет их одним сетевым рейсом: N команд — один рейс вместо N.
  • rdb.Pipelined принимает замыкание с планированием и возвращает срез Cmder; значение конкретной команды — через Val() типизированной переменной.
  • Пайплайн не атомарен: между вашими командами сервер выполняет команды других клиентов; для «прочитал — решил — записал» нужны транзакции с WATCH.
  • Батч держите в сотнях команд: миллионный батч раздувает выходной буфер Redis до разрыва соединения.
  • err от Pipelined — первая ошибка из результатов; redis.Nil от Get внутри батча — штатный промах, а не авария.
  • Прогрев кэша, пачки счётчиков и мультиключевые чтения — типовые места пайплайнов в Go-сервисах.

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

Соберите warmCache с батчами по 500 и по 50 и прогрейте тысячу тестовых элементов локальным Go с Redis в Docker: замерьте время каждого варианта таймером. Затем перепишите хендлер с тремя rdb.Get на один Pipelined и проверьте в redis-cli MONITOR, что команды приходят одним куском. В конце добавьте Get по отсутствующему ключу внутрь пайплайна и обработайте redis.Nil у конкретной команды, не топя весь батч.

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

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

Чем Pipelined отличается от Pipeline?

Pipelined создаёт пайплайн, выполняет ваше замыкание и сразу ждёт результаты, возвращая срез Cmder. Pipeline отдаёт объект Pipeliner, которым вы наполняете команды в разных местах кода и потом вызываете Exec. Под капотом это один и тот же буфер и одно соединение из пула.

Замедляет ли большой пайплайн Redis для других клиентов?

Само батчирование нет: команды те же. Следите за размером: гигантский батч раздувает выходной буфер сервера, и при срабатывании client-output-buffer-limit соединение рвётся. Сотни команд на батч — безопасная практика, которую ломать незачем.

Нужен ли пайплайн, если команды вызывают много горутин?

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

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

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

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

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