$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогКэш и инвалидация на практике: write-through, write-back и дедупликация запросов

Кэш и инвалидация на практике: write-through, write-back и дедупликация запросов

$ sudo teach IT
·23 июля 2026 г.·10 мин·63
Кэш и инвалидация на практике: write-through, write-back и дедупликация запросов

Разберём рабочие паттерны кэширования для конкурентных обновлений и покажем, как предотвращать «шторм» при промахе кэша.

Содержание
1. Термины и модель: что именно кэшируем1.1. Кэш как копия с заданной семантикой1.2. Инвалидация vs вытеснение по TTL2. Стратегии записи: write-through и write-back2.1. Write-through: запись в кэш и источник одновременно2.2. Write-back: запись только в источник, а кэш догоняет позже3. Инвалидация: “почистить ключ” — недостаточно3.1. Два подхода к инвалидации3.2. Гонка чтения и записи: сценарий, который сложно увидеть4. Предотвращение “штормов”: дедупликация запросов (single-flight)4.1. Что такое single-flight4.2. Паттерн дедупликации поверх Redis/кэша5. Write-through + инвалидация + дедупликация: практический сценарий5.1. Модель данных для кэша5.2. Пример: дедупликация при промахе (JavaScript/TypeScript)5.3. Ограничения и улучшения6. Комбинации write-through/write-back с дедупликацией6.1. Write-through + дедупликация: меньше промахов, но lock всё равно нужен6.2. Write-back + дедупликация: критически важно6.3. Инвалидация как триггер штормов7. Подводные камни инвалидации и дедупликации7.1. Неправильный порядок действий при обновлении7.2. “Слепая” дедупликация без учёта версии7.3. Кэширование ошибок7.4. TTL и “края” событий8. Набор практических решений (checklist)

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

В этой статье разберём три практических аспекта кэширования:

  1. Как выбирать стратегию записи: write-through vs write-back (и почему обе могут быть опасны при неправильной инвалидации).
  2. Как делать инвалидацию корректно: где ошибаются команды, которые «просто чистят ключи».
  3. Как предотвращать “шторм” промахов: паттерн дедупликации запросов (single-flight / request coalescing) и смежные техники.

Цель — чтобы вы могли не гадать, а проектировать поведение кэша как часть системы согласования данных.


1. Термины и модель: что именно кэшируем

Прежде чем выбирать паттерн, полезно зафиксировать, что кэш хранит и какие отношения должны соблюдаться между «истиной» (источник данных) и кэшем.

1.1. Кэш как копия с заданной семантикой

В большинстве приложений кэш — это:

  • либо копия данных с оговорённым временем жизни (TTL),
  • либо результат дорогой операции (например, сборка профиля),
  • либо агрегат/вычисление, обновляемое по событию.

Главный вопрос: насколько допустимо видеть устаревшие данные и на каком уровне.

  • Strong consistency требует обновлять все чтения синхронно — на практике дорого и не всегда возможно.
  • Eventual consistency допускает кратковременную рассинхронизацию, компенсируя её TTL/версией/событиями.

1.2. Инвалидация vs вытеснение по TTL

Есть два фундаментальных механизма:

  • TTL (expiration): кэш протухает сам. Простота, но возможны «гонки» при одновременном протухании.
  • Инвалидация: мы явным образом помечаем ключ как невалидный (удаляем, обновляем версию, публикуем событие).

Проблема начинается, когда комбинируют TTL и ручную инвалидацию, не понимая, что происходит при гонках.


2. Стратегии записи: write-through и write-back

2.1. Write-through: запись в кэш и источник одновременно

write-through означает, что при обновлении данных приложение делает:

  1. запись в источник (БД/сервис),
  2. синхронную/полусинхронную запись в кэш (или наоборот, но чаще источник и кэш должны завершиться «как часть одной логики»).

Плюсы:

  • кэш быстро отражает изменения,
  • меньше случаев чтения устаревшего.

Минусы:

  • выше задержка на запись (кэш становится частью критического пути),
  • при отказах кэша или проблемах с сетью можно получить каскадные эффекты.

Практический рецепт

Write-through чаще применяют, когда:

  • чтение в разы чаще записи,
  • кэш надёжен (например, локальный инстанс/кластер с хорошей отказоустойчивостью),
  • допускается увеличение latency на write.

Но важно: кэш не должен «переписать» более новые данные более старыми. Для этого часто используют версионирование/ETag/временные метки.

2.2. Write-back: запись только в источник, а кэш догоняет позже

write-back — это стратегия, при которой запись обновляет источник, а кэш либо:

  • не трогается, пока не будет промах/инвалидация,
  • обновляется лениво (например, при следующем чтении).

Плюсы:

  • запись дешевле,
  • меньше зависимости кэша в момент обновления.

Минусы:

  • риск чтения устаревших значений между обновлением в источнике и обновлением/инвалидацией кэша,
  • сложнее бороться с «штормом» после TTL/инвалидации.

Где чаще всего ломается write-back

Типичный сценарий:

  1. ключ протухает (TTL истёк) или его инвалидация сработала,
  2. приходит много запросов на чтение,
  3. каждый делает обращение к источнику (БД/сервису),
  4. источник перегружается и начинает отвечать медленно/с ошибками,
  5. клиенты ретраят, усиливая нагрузку.

Это и есть тот самый кэш-шторма при промахе кэша.


3. Инвалидация: “почистить ключ” — недостаточно

Инвалидация — это не действие «delete key». Это часть протокола согласования между источником и кэшем.

3.1. Два подхода к инвалидации

(A) Удаление (delete) ключа

После обновления данных вы удаляете запись в кэше:

  • просто,
  • эффективно устраняет устаревшие значения,
  • но может привести к шторму, если ключ протухает/удаляется массово.

(B) Версионирование (versioned keys)

Вместо удаления вы меняете «версию» ключа:

  • храните cache_version[userId],
  • формируете кэш-ключ как profile:{userId}:v{version},
  • при обновлении инкрементируете версию или публикуете событие.

Плюсы:

  • отсутствие гонок «удалили, пока кто-то читает»,
  • легче обеспечивать атомарность через CAS/Redis Lua,
  • можно держать несколько версий на время.

Минусы:

  • рост количества ключей (нужно GC/TTL на старые версии),
  • усложнение дизайна.

3.2. Гонка чтения и записи: сценарий, который сложно увидеть

Рассмотрим без версионирования:

  • Поток A делает обновление в БД.
  • Поток A удаляет ключ в кэше.
  • Поток B в этот момент читает ключ:

Варианты:

  1. B успел прочитать кэш до удаления — получит старое значение.
  2. B успел прочитать кэш после удаления — получит промах и пойдёт в БД.
  3. B пришёл между записью в БД и удалением из кэша — получит старое значение, если ключ ещё не удалён.

Если вы используете write-back с инвалидацией, критична порядковость событий: инвалидация должна отражать момент истины. На практике это означает либо транзакционную связь, либо «временной маркер» (версии/лейблы) и проверку при записи в кэш.


4. Предотвращение “штормов”: дедупликация запросов (single-flight)

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

4.1. Что такое single-flight

Идея: при промахе кэша создаётся «координация» для конкретного ключа:

  • первый запрос запускает дорогое вычисление/чтение,
  • остальные запросы на тот же ключ не запускают повторно, а ждут тот же future/promise,
  • по завершении результата кэш заполняется, и все возвращают одно значение.

Подводные камни:

  • время ожидания (таймауты),
  • обработка ошибок (если вычисление упало — как отвечать всем?),
  • очистка ожиданий (чтобы не протечь память/локи).

4.2. Паттерн дедупликации поверх Redis/кэша

Есть несколько уровней реализации:

  1. Внутри процесса (локальный single-flight): хорошо для одного инстанса.
  2. В масштабе (распределённый single-flight): сложнее, но правильно для нескольких инстансов.

Для распределённого варианта обычно используют:

  • распределённый lock (Redis SET NX PX),
  • Pub/Sub или Streams для ожидания результата,
  • либо специальные конструкции в приложении (например, “promise registry” в shared store).

Ниже — рабочие примеры на уровне приложения с Redis как координацией.


5. Write-through + инвалидация + дедупликация: практический сценарий

Представим API: GET /profile/{userId} собирает профиль из БД (дорого), а POST /profile/{userId} обновляет данные.

5.1. Модель данных для кэша

  • key: profile:{userId}:v{version}
  • версию храним в отдельном ключе или в метаданных источника.
  • при обновлении данных увеличиваем версию и инвалидируем старую.

Если вы не хотите версионирование — можно делать delete, но тогда больше шансов словить гонки и шторм.

5.2. Пример: дедупликация при промахе (JavaScript/TypeScript)

Ниже пример логики на Node.js. Он показывает идею single-flight через Redis lock. Это не единственный путь, но он понятный и рабочий.

Утилиты: чтение/запись кэша и lock

code
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

const CACHE_TTL_SECONDS = 300;
const LOCK_TTL_MS = 5000;
const WAIT_POLL_MS = 50;

async function getCachedProfile(key: string) {
  const raw = await redis.get(key);
  if (!raw) return null;
  return JSON.parse(raw);
}

async function setCachedProfile(key: string, value: any) {
  await redis.set(key, JSON.stringify(value), { EX: CACHE_TTL_SECONDS });
}

async function acquireLock(lockKey: string, token: string) {
  // SET key value NX PX ttl
  const res = await redis.set(lockKey, token, { NX: true, PX: LOCK_TTL_MS });
  return res === "OK";
}

async function releaseLock(lockKey: string, token: string) {
  // Снять lock безопасно через Lua, проверив токен
  const lua = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  `;
  await redis.eval(lua, { keys: [lockKey], arguments: [token] });
}

Handler чтения с дедупликацией

code
function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function waitForCache(key: string, timeoutMs: number) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const cached = await getCachedProfile(key);
    if (cached) return cached;
    await sleep(WAIT_POLL_MS);
  }
  return null;
}

async function computeProfileFromDB(userId: string) {
  // имитация дорогого вычисления
  await sleep(200);
  return { userId, name: "Alice", updatedAt: Date.now() };
}

export async function getProfile(userId: string) {
  // Предположим, version берётся из источника/метаданных
  const version = await redis.get(`profile_version:${userId}`) ?? "1";
  const cacheKey = `profile:${userId}:v${version}`;

  const cached = await getCachedProfile(cacheKey);
  if (cached) return cached;

  // Дедупликация: один вычислитель на ключ
  const lockKey = `lock:profile:${userId}:v${version}`;
  const token = `${process.pid}:${Date.now()}:${Math.random()}`;

  const hasLock = await acquireLock(lockKey, token);

  if (hasLock) {
    try {
      // Делаем реальную работу
      const value = await computeProfileFromDB(userId);
      await setCachedProfile(cacheKey, value);
      return value;
    } finally {
      await releaseLock(lockKey, token);
    }
  } else {
    // Другие запросы ждут, когда первый заполнит кэш
    const value = await waitForCache(cacheKey, 3000);
    if (value) return value;

    // Если таймаут — можно решать по стратегии:
    // 1) снова попытаться,
    // 2) один раз сделать вычисление,
    // 3) вернуть ошибку/статус.
    // Здесь — ещё одна попытка чтения напрямую (осторожно!)
    const fallback = await computeProfileFromDB(userId);
    await setCachedProfile(cacheKey, fallback);
    return fallback;
  }
}

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

  • В момент промаха только один инстанс держит lock.
  • Остальные не стучат в БД — они ждут заполнения кэша.
  • TTL lock ограничивает зависшие вычисления.

5.3. Ограничения и улучшения

  1. Lock TTL должен покрывать worst-case время вычисления плюс запас. Иначе вы получите ситуацию, когда второй инстанс тоже начнёт вычисление.
  2. Поллинг (waitForCache) может добавить нагрузку. Более изящно — использовать Pub/Sub/Streams: первый вычислитель публикует событие, остальные ждут.
  3. Пустые значения. Если профиль отсутствует, лучше кэшировать “not found” с коротким TTL, иначе шторм при повторяющихся запросах на несуществующий ключ гарантирован.

6. Комбинации write-through/write-back с дедупликацией

Теперь свяжем стратегии записи с тем, как инвалидация и дедупликация влияют на поведение.

6.1. Write-through + дедупликация: меньше промахов, но lock всё равно нужен

Если при обновлении вы синхронно обновляете кэш, то промахов меньше. Однако промахи всё равно бывают:

  • ключи впервые запрашиваются после деплоя,
  • инвалидация отработала,
  • сбой кэша/сети потерял запись,
  • истёк TTL.

Значит single-flight остаётся полезным, просто реже включается.

6.2. Write-back + дедупликация: критически важно

При write-back вы сознательно позволяете кэшу быть устаревшим на коротком интервале. Если вы делаете инвалидацию через delete, то после инвалидации кэш гарантированно промахнётся и будет нужен single-flight, чтобы не устроить лавину на БД.

Версионирование может снизить число промахов (старую версию можно держать, пока новая не соберётся), но при первом запросе на новую версию всё равно будет промах.

6.3. Инвалидация как триггер штормов

Есть распространённая ошибка: вы удаляете много ключей разом (например, “очистим профиль пользователя во всех местах”). В момент массового удаления кэш станет пустым, и дедупликация по одному ключу не спасёт, если у вас миллион уникальных ключей одновременно.

Тут помогают:

  • плавные обновления (batching, rate limiting инвалидации),
  • версионирование вместо массового delete,
  • pre-warm горячих ключей (иногда),
  • корректный TTL: не синхронизировать экспирацию на одну секунду.

7. Подводные камни инвалидации и дедупликации

7.1. Неправильный порядок действий при обновлении

Если вы обновляете источник и затем удаляете кэш, но без гарантии, что запись в БД стала видна, то читатели могут:

  • увидеть старое значение (если кэш ещё не удалён),
  • или попасть в промах и загрузить старые данные (если кэш-вычисление читает из источника, который в процессе обновления ещё не завершён).

Решения:

  • версионирование: кэш-ключ соответствует версии данных,
  • транзакционная схема, где версия меняется атомарно с данными,
  • outbox pattern + фоновой публикацией событий.

7.2. “Слепая” дедупликация без учёта версии

Если вы дедуплицируете по userId, но версия могла смениться, то второй поток может ждать вычисления для старой версии и затем записать её обратно в кэш (если ключ одинаковый по логике). Важно, чтобы lock и кэш-ключ включали версию/инвалидационный маркер.

Правило: координация вычислений должна быть привязана к точной цели кэша, а не к абстрактному “профиль пользователя”.

7.3. Кэширование ошибок

Если вычисление падает (ошибка БД), лучше не кэшировать “ошибку навсегда”, но полезно:

  • быстро отвечать одинаковой ошибкой всем ожидающим,
  • кэшировать временный “fallback” или “circuit breaker” на короткий срок,
  • чтобы избежать повторного шторма, пока источник не восстановился.

7.4. TTL и “края” событий

Если TTL равен 300 секунд и вы удаляете ключи вручную, моменты протухания могут совпасть. Типичная техника — jitter: слегка рандомизировать TTL, чтобы экспирации распределялись.

Например: EX = baseTTL * (0.9 + random*0.2). Для больших систем это существенно снижает вероятность одновременного промаха.


8. Набор практических решений (checklist)

Ни

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

Перейти к курсу

Другие статьи

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1790
GitHub Actions на практике: автоматизируем тесты, линтинг и деплой за один вечер
github

GitHub Actions на практике: автоматизируем тесты, линтинг и деплой за один вечер

Пишем реальный workflow с нуля: запуск pytest, проверка кода через Ruff и автоматический деплой на сервер при пуше в main. Разбираем структуру YAML-конфига и типичные ошибки.

18 июля 2026 г.
1190
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1960
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
20
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1960
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
130

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

1. Термины и модель: что именно кэшируем1.1. Кэш как копия с заданной семантикой1.2. Инвалидация vs вытеснение по TTL2. Стратегии записи: write-through и write-back2.1. Write-through: запись в кэш и источник одновременно2.2. Write-back: запись только в источник, а кэш догоняет позже3. Инвалидация: “почистить ключ” — недостаточно3.1. Два подхода к инвалидации3.2. Гонка чтения и записи: сценарий, который сложно увидеть4. Предотвращение “штормов”: дедупликация запросов (single-flight)4.1. Что такое single-flight4.2. Паттерн дедупликации поверх Redis/кэша5. Write-through + инвалидация + дедупликация: практический сценарий5.1. Модель данных для кэша5.2. Пример: дедупликация при промахе (JavaScript/TypeScript)5.3. Ограничения и улучшения6. Комбинации write-through/write-back с дедупликацией6.1. Write-through + дедупликация: меньше промахов, но lock всё равно нужен6.2. Write-back + дедупликация: критически важно6.3. Инвалидация как триггер штормов7. Подводные камни инвалидации и дедупликации7.1. Неправильный порядок действий при обновлении7.2. “Слепая” дедупликация без учёта версии7.3. Кэширование ошибок7.4. TTL и “края” событий8. Набор практических решений (checklist)