awesome-everything EN
↑ Обратно к восхождению

Кеширование

Лок и single-flight: ограничение параллельных rebuild

Суть Redis SETNX-лок сериализует rebuild через весь флот; in-process single-flight схлопывает per-node стадо до одного Promise без сетевых затрат. Нужны оба слоя последовательно.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

На границе TTL 100 000 запросов бьют во флот из 50 нод. Каждая нода запускает in-process single-flight. Сколько запросов к БД происходит? Не 100 000 — но и не 1. Ответ показывает ровно то, где каждый слой митигации помогает, а где нет.

Митигация 1: распределённый лок с SETNX

Простейшая cross-node митигация: перед запуском rebuild захватить лок. Redis-примитив:

SET lock:key uuid EX 30 NX
  • NX — установить, только если ключ не существует (set-if-not-exists).
  • EX 30 — авто-истечение через 30 с (страховка от упавших rebuilder-ов).
  • uuid — уникальный токен захватывающего процесса (используется для fencing, рассматривается в старшем уроке).

Что происходит на истечении:

  1. Запрос-1 приходит. Делает SET lock:homepage:v1 uuid-A EX 30 NX → успех. Запускает rebuild.
  2. Запросы 2–N приходят. Делают тот же SET → провал (NX). Видят, что лок занят.
  3. Вариант А: каждый ожидающий перепроверяет кеш через короткое ожидание (50–200 мс). К тому моменту rebuild может завершиться.
  4. Вариант Б: каждый ожидающий немедленно возвращает fallback (stale-значение, дефолт-страницу, пустой 204).
  5. Запрос-1 завершает rebuild. Пишет новое значение. Удаляет лок.

EX=30 — не TTL кеша, а страховка. Если rebuilder падает на шаге 1 без удаления лока, лок авто-истекает через 30 с. EX должен быть больше rebuild p99, но достаточно коротким, чтобы падение не глушило трафик надолго. Типичная цель: 3× среднее время rebuild.

СценарийБез локаС SETNX локом
10-нодный флот, 2 000 promis/нода20 000 параллельных запросов к БД10 запросов к БД (1 на ноду) или 1 с cross-node локом
Rebuilder падает в середине работыСтадо повторяется каждый TTLЛок авто-истекает после EX секунд
EX лока слишком короткийN/AВторой rebuilder гонится — дублирование записей

Митигация 2: in-process single-flight

Распределённые локи координируют через флот; single-flight координирует внутри одного процесса. Никакого Redis, никакого сетевого round-trip — только in-process map.

Паттерн:

type SingleFlight struct { mu sync.Mutex; inflight map[string]*call }
  1. Запрос приходит; cache miss.
  2. Проверить in-process map по ключу. Если Promise / call уже существует → подписаться на него, ждать resolve, вернуть общий результат.
  3. Если нет записи → создать новую (Promise), запустить rebuild, добавить в map.
  4. Когда rebuild завершается → resolve Promise, удалить из map. Все подписчики получают результат одновременно.

Стандартная библиотека Go поставляет это как singleflight.Group.Do. Эквиваленты Node.js: p-memoize или ручной паттерн Map<key, Promise>.

Стоимость: O(1) lookup в памяти процесса. Без сети. Без acquire лока.

Область действия: только per-process. Флот из 50 нод имеет 50 независимых in-process map. На границе TTL с 100 000 параллельными запросами, равномерно распределёнными, single-flight в одиночку даёт 50 запросов к БД (1 на ноду), не 100 000. Добавь распределённый лок, чтобы перейти от 50 к 1.

Почему это работает

Facebook «memcache leases» (Nishtala et al., NSDI 2013) реализуют ту же идею на уровне кеша: при miss кеш возвращает 64-битный lease-токен. Только клиент, держащий токен, может писать обратно. Параллельные miss-клиенты получают null без токена и им говорят ждать. Результат: пиковый QPS БД упал с 17 000 до 1 300 — примерно 13× — на одном hot-key кластере.

Композиция обоих слоёв

Ни один слой не достаточен сам по себе:

  • Только single-flight: 50-нодный флот всё равно шлёт 50 параллельных rebuild.
  • Только распределённый лок: ожидающие (все кроме 1 lock-holder-а) ничего не получают, пока rebuild идёт — добавляет латентность каждому запросу на границе.

Совмещённый стек:

  1. Проверить in-process map → если Promise в полёте, подписаться и ждать.
  2. Promise нет → попробовать SET lock:key uuid EX 30 NX.
  3. Лок захвачен → зарегистрировать Promise, запустить rebuild, записать значение, удалить лок, resolve Promise.
  4. Лок НЕ захвачен → ретрай GET кеша через 50 мс. Вернуть stale fallback, если всё ещё пусто.
Расставь шаги по порядку

Поставь шаги, которые делает запрос в single-flight + Redis-lock стеке:

  1. 1 Cache GET возвращает nil (miss)
  2. 2 Проверить in-process singleflight map — если Promise существует, подписаться на него
  3. 3 Promise нет: попробовать SET lock:key uuid EX 30 NX
  4. 4 Лок захвачен: зарегистрировать новый Promise и запустить rebuild
  5. 5 Rebuild завершён: записать значение в кеш с TTL, удалить Redis-лок, resolve Promise
  6. 6 Все in-process подписчики получают результат через общий Promise
  7. 7 Лок НЕ захвачен: ждать 50 мс, перепроверить кеш, вернуть stale fallback если всё ещё пусто
Викторина

Флот из 50 нод использует только in-process single-flight. На границе TTL приходит 100 000 параллельных misses. Сколько rebuild к БД?

Викторина

Какова роль значения EX в SETNX-based локе?

Викторина

Лок кеша использует EX=10 с. Rebuild занимает 12 с. Что происходит?

Вспомните перед уходом
  1. 01
    Какая практическая разница между in-process single-flight и Redis distributed lock, и когда использовать каждый?
  2. 02
    Facebook memcache leases (NSDI 2013) снизили пиковый QPS БД с 17 000 до 1 300. Какой механизм этого достигает?
  3. 03
    Почему EX лока должен быть установлен больше rebuild p99, а не просто среднего?
Итог

Два механизма ограничивают параллельные rebuild без изменения TTL кеша. In-process single-flight поддерживает per-process map in-flight Promise-ов; каждый запрос, приходящий пока rebuild запущен, подписывается на тот же Promise вместо нового rebuild — нулевые сетевые затраты, нулевая координация. Redis SETNX distributed lock сериализует rebuild через весь флот с помощью SET key uuid EX N NX acquire и явного удаления по завершении. Композиция обоих снижает 100 000 параллельных misses на 50-нодном флоте до 1 запроса к БД. EX лока должен превышать rebuild p99; пара лок + stale fallback так, чтобы ожидающие никогда не блокировались бесконечно.

Связанные уроки
встречается в202
Продолжить восхождение ↑XFetch: вероятностное раннее истечение без координации
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.