B04 · Кэширование#
Самый дешёвый способ ускорить чтение — и самый частый источник ответов «данные не обновились». Кэш вводит вторую копию данных, а вместе с ней вопрос согласованности, которого до этого не было.
Карточки идут от уровней кэша к стратегиям чтения и записи, инвалидации, патологиям и Redis.
Навигация: CACHE-01 · CACHE-02 · CACHE-03 · CACHE-04 · CACHE-05
CACHE-01 · Уровни кэша: что и где кэшировать#
Шаг W5
TL;DR. Кэш есть на каждом уровне пути запроса. Самый дешёвый — тот, что ближе к пользователю; самый управляемый — тот, что ближе к данным.
Проблема. «Добавим кэш» — не одно решение, а выбор из шести мест. У каждого своя цена, свой радиус действия и свои проблемы с устареванием. Кэш не на своём месте либо не даёт эффекта, либо создаёт неуправляемое устаревание у миллионов клиентов.
Как работает — уровни от клиента к данным.
| Уровень | Что кэширует | Управляемость | Радиус |
|---|---|---|---|
| Браузер / приложение клиента | Статику, ответы API по заголовкам | Низкая: нельзя инвалидировать удалённо | Один пользователь |
| CDN / edge (EDGE-05) | Статику, публичные ответы | Средняя: есть purge (принудительная очистка) | Регион |
| Reverse proxy (обратный прокси: nginx, Varnish) | Ответы целиком | Высокая | Дата-центр |
| Кэш в памяти процесса | Горячие объекты, конфигурацию | Полная, но у каждого экземпляра свой | Один экземпляр |
| Распределённый кэш (Redis, Memcached) | Объекты, сессии, результаты запросов | Полная, общая для всех | Кластер |
| Буферный пул БД | Страницы данных и индексов | Автоматическая | Одна БД |
Что кэшировать. Хорошие кандидаты: данные, которые читают гораздо чаще, чем меняют; результаты дорогих вычислений и агрегаций; ответы внешних сервисов. Плохие: данные, читаемые однократно; результаты, уникальные для каждого запроса; всё, где устаревание недопустимо.
Правило порядка. Сначала найти узкое место (W1, W5), потом кэшировать. Кэш поверх неоптимального запроса прячет проблему до первого промаха.
Trade-offs и режимы отказа. Каждый уровень добавляет копию со своим временем жизни. Одно и то же изменение доходит до пользователя через разные уровни за разное время, и со стороны это выглядит как хаотичное поведение. Кэш в памяти процесса при N экземплярах даёт N разных состояний: для конфигурации это приемлемо, для данных пользователя — опасно. Кэш с неправильной инвалидацией опаснее, чем отсутствие кэша.
Что мерить.
- hit ratio (долю запросов, которые обслужил кэш) по каждому уровню: при низком hit ratio кэш бесполезен, но по-прежнему занимает память и вносит рассогласование;
- задержку с попаданием и без;
- объём и вытеснение;
- возраст отдаваемых данных.
Связано: CACHE-02 стратегии · CACHE-03 инвалидация · EDGE-05 CDN · NET-06 HTTP-кэширование
Источники: AWS — Caching Best Practices · MDN — HTTP caching · ByteByteGo — Cache Systems Every Developer Should Know
CACHE-02 · Стратегии чтения и записи#
Шаг W5
TL;DR. Стратегия определяет, кто загружает и записывает данные — приложение или кэш. От неё зависит, что произойдёт при падении кэша и какие данные потеряются.
Проблема. Кэш и БД — два хранилища, и обновляются они не атомарно. Порядок операций определяет, какие рассогласования возможны и что теряется при отказе каждой стороны. Стратегия, выбранная наугад, даёт ошибки, которые воспроизводятся раз в тысячу запросов.
Стратегии чтения.
- Cache-aside (lazy loading, ленивая загрузка) — приложение проверяет кэш, при промахе читает БД и кладёт результат в кэш. Самая распространённая стратегия: в кэше лежит только то, что действительно запрашивали, а падение кэша нагружает БД, но не теряет данные. При записи нужна явная инвалидация.
- Read-through — приложение читает только через кэш, а при промахе кэш сам загружает данные. Логика загрузки собрана в одном месте, но её должна поддерживать библиотека или сервис кэша.
- Refresh-ahead — популярные ключи обновляются заранее, до истечения TTL (time to live — срока жизни ключа). Это снижает задержку и защищает от одновременного истечения, но тратит ресурсы на данные, которые могут и не понадобиться.
Стратегии записи.
- Write-through (сквозная запись) — запись идёт в кэш и в БД синхронно. Кэш актуален, но каждая запись дольше на одну операцию.
- Write-behind (write-back, отложенная запись) — запись идёт в кэш, а в БД — позже, асинхронно и пачками. Самая быстрая запись — и единственная стратегия, при которой падение кэша означает потерю данных. Годится для счётчиков и телеметрии, не годится для денег.
- Write-around — запись идёт мимо кэша, прямо в БД; кэш наполняется только при чтении. Подходит для данных, которые пишут часто, а читают редко.
Типичная комбинация. Cache-aside для чтения плюс инвалидация (удаление ключа) при записи. Строгой согласованности эта пара не даёт, зато ведёт себя предсказуемо и переживает падение кэша.
Альтернативы
| Комбинация | Когда лучше | Цена |
|---|---|---|
| Cache-aside + delete on write | Умолчание для большинства систем | Гонки при параллельных чтении и записи (CACHE-03) |
| Write-through + read-through | Нужна максимальная свежесть кэша | Медленнее запись, кэш держит всё |
| Write-behind | Огромный поток записи, потери допустимы | Потеря данных при отказе кэша |
| Write-around | Пишут много, читают мало | Первое чтение — промах |
Trade-offs и режимы отказа. У cache-aside есть окно для гонки. Процесс А прочитал из БД старое значение. Тем временем процесс Б обновил БД и удалил ключ. Затем А кладёт старое значение в кэш — после удаления, и оно живёт там до истечения TTL. Окно сужают короткий TTL, версионирование ключа или повторное удаление с задержкой. Write-behind без надёжно сохранённой очереди изменений при аварии гарантированно теряет данные.
Что мерить.
- hit ratio (долю попаданий);
- долю устаревших ответов — выборочным сравнением с БД;
- задержку записи с кэшем и без;
- объём и возраст неподтверждённых записей при write-behind.
Связано: CACHE-03 инвалидация · CACHE-04 патологии · MSG-07 outbox
Источники: AWS — Caching patterns · Scaling Memcache at Facebook (NSDI 2013) · ByteByteGo — Cache Systems Every Developer Should Know
CACHE-03 · Инвалидация и согласованность с БД#
Шаг W6
TL;DR. Инвалидировать кэш можно тремя способами: по времени, по событию и по версии ключа. Первый прост и переживает любые сбои, третий надёжен и исключает гонки, второй — посередине.
Проблема. Данные изменились в БД, а в кэше лежит старая копия. Простое «обновим кэш после записи» проигрывает гонку: два процесса могут записать значения в кэш в порядке, обратном порядку записи в БД. Простое «удалим ключ после записи» оставляет окно: параллельное чтение успевает вернуть в кэш устаревшее значение (CACHE-02).
Как работает.
По времени (TTL, time to live). Ключ живёт заданный срок. Способ простой, переживает любые сбои и не требует координации — поэтому его применяют чаще всего. Цена: данные устаревают на величину TTL, а массовое одновременное истечение вызывает всплеск (CACHE-04). На практике: короткий TTL для изменчивых данных плюс небольшой jitter (случайный разброс) срока жизни.
По событию. При изменении данных публикуется событие, а потребитель удаляет или обновляет ключи. Данные свежее, но появляется зависимость от доставки: потерянное событие оставляет кэш устаревшим бессрочно. Поэтому TTL всё равно оставляют как страховку. Надёжный источник событий — журнал изменений БД (CDC, change data capture — захват изменений данных; MSG-07). Вызов из кода приложения ненадёжен: при откате транзакции он может не выполниться.
По версии ключа. Ключ включает версию сущности или метку изменения: user:42:v17. Обновление данных меняет версию, и новый запрос не находит старый ключ: инвалидация не нужна вовсе, гонок нет. Старые ключи вытесняются сами. Цена: текущую версию нужно где-то дёшево читать — обычно она уже есть в строке БД или в отдельном счётчике.
Как выбирать.
- устаревание на секунды допустимо → TTL;
- свежесть нужна почти сразу, данные важные → CDC-события плюс TTL как страховка;
- данные часто читают и редко меняют, гонки недопустимы → версионирование ключа.
Trade-offs и режимы отказа. Инвалидация из кода приложения происходит вне транзакции. Если транзакция откатилась, а кэш уже очищен, — не страшно. Если кэш уже обновлён новым значением — страшно: в кэше данные, которых нет в БД. Массовая инвалидация по событию (изменился справочник — сбросили тысячи ключей) вызывает лавину промахов. Если кэш инвалидируется только по событиям, то при сбое конвейера он отдаёт устаревшие данные неограниченно долго.
Что мерить.
- возраст отдаваемых данных;
- долю расхождений при выборочной сверке с БД;
- всплески промахов после инвалидации;
- отставание конвейера событий.
Связано: CACHE-02 стратегии · CACHE-04 патологии · MSG-07 CDC и outbox · DIST-03 модели согласованности
Источники: Scaling Memcache at Facebook (NSDI 2013) · Redis — Client-side caching · ByteByteGo — Caching Pitfalls Every Developer Should Know
CACHE-04 · Патологии: stampede, hot key, negative caching#
Шаг W7
TL;DR. Кэш редко деградирует плавно: он либо помогает, либо резко перестаёт помогать — и роняет БД, которая давно не рассчитана на полную нагрузку.
Проблема. БД под кэшем видит 5 % запросов. Если кэш исчезает или массово промахивается, на БД приходит нагрузка в 20 раз больше обычной. Она не справляется, ответы замедляются, клиенты повторяют запросы — и система входит в устойчивое состояние отказа. Из него она не выходит даже после восстановления кэша.
Патологии и лечение.
Cache stampede (dogpile). Популярный ключ истёк — тысяча параллельных запросов одновременно идёт в БД за одним и тем же значением.
- Блокировка на пересчёт: первый запрос берёт короткую блокировку и пересчитывает значение, остальные ждут или получают устаревшее.
- Request coalescing (объединение запросов) на уровне процесса: одна загрузка на все ожидающие корутины.
- Probabilistic early expiration (вероятностное раннее обновление): чем ближе истечение TTL, тем выше шанс, что очередной запрос обновит значение заранее. Нагрузка размазывается во времени.
- stale-while-revalidate: отдать устаревшее и обновить в фоне.
Массовое одновременное истечение. Ключи, созданные в один момент (после выкатки, после прогрева), истекают одновременно. Лечится добавлением jitter (случайного разброса) к TTL (±10–20 %).
Hot key (горячий ключ). Один ключ получает непропорциональную долю запросов (звезда в соцсети, товар на распродаже). Шардирование не помогает — ключ живёт на одном узле. Что помогает:
- локальная копия в памяти процесса на короткий срок;
- репликация hot key на несколько узлов и чтение со случайного;
- разбиение значения на N подключей, если это счётчик или список.
Запросы за несуществующим. Ответ «данных нет» по умолчанию не кэшируется, и такие запросы каждый раз доходят до БД. Это обычный вектор перегрузки, в том числе намеренной. Лечится negative caching (негативным кэшированием) — кэшированием самого факта отсутствия с коротким TTL — и фильтром Блума перед хранилищем.
Холодный старт. После перезапуска кэш пуст, и полный трафик приходит на БД. Лечится прогревом ключевых данных перед вводом в ротацию (REL-05) и постепенным вводом трафика.
Сводка
| Патология | Признак | Лечение |
|---|---|---|
| Cache stampede | Всплеск обращений к источнику в момент истечения ключа | Блокировка на пересчёт, request coalescing, probabilistic early expiration |
| Массовое одновременное истечение | Периодические пики нагрузки с равным интервалом | Jitter срока жизни ±10–20 % |
| Hot key | Перекос обращений на один узел кэша | Локальная копия, репликация ключа, разбиение значения |
| Запросы за несуществующим | Промахи без роста попаданий | Negative caching, фильтр Блума перед хранилищем |
| Холодный старт | Всплеск на источнике после перезапуска | Прогрев до ввода в ротацию, постепенный ввод трафика |
Trade-offs и режимы отказа. Блокировка на пересчёт превращает популярный ключ в точку сериализации. Отдача устаревших данных при сбое (stale-if-error) повышает доступность ценой корректности; включать её или нет, решают по конкретным данным, а не по умолчанию. Локальные копии hot key возвращают проблему N разных состояний (CACHE-01).
Что мерить.
- hit ratio (долю попаданий) и его провалы;
- распределение обращений по ключам (топ-N);
- число одновременных пересчётов одного ключа;
- нагрузку на БД в момент падения hit ratio;
- долю ответов, отданных устаревшими.
Связано: CACHE-03 инвалидация · REL-01 таймауты и повторы · EDGE-06 rate limiting · DATA-11 consistent hashing
Источники: Optimal Probabilistic Cache Stampede Prevention (VLDB 2015) · Google SRE Book, гл. 22 «Addressing Cascading Failures» · RFC 5861 — stale-while-revalidate · ByteByteGo — Caching Pitfalls Every Developer Should Know
CACHE-05 · Redis#
Шаг W5
TL;DR. Redis — хранилище структур данных в памяти, которое выполняет команды в одном потоке. Он быстр не вопреки однопоточности, а во многом благодаря ей.
Проблема. Кэш «ключ → строка» решает не всё: нужны счётчики, очереди, множества, рейтинги, rate limiting (ограничение частоты), распределённые примитивы. Реализовать это поверх обычного хранилища «ключ — значение» (KV) можно только с атомарностью, которой там нет.
Как работает.
- Структуры данных как примитивы: строки, хэши, списки, множества, упорядоченные множества (sorted set), битовые карты, HyperLogLog (приблизительный подсчёт уникальных), геоиндексы, потоки (Streams). Операции над ними атомарны.
- Однопоточное выполнение команд. Нет блокировок, нет переключений контекста, нет гонок — каждая команда атомарна по построению. Узкое место обычно не CPU, а сеть и память. Поэтому современные версии выносят сетевой ввод-вывод в отдельные потоки, но сами команды по-прежнему выполняют последовательно.
- Персистентность: RDB (периодический снимок) и AOF (журнал команд). Даже с AOF и
fsync everysecможно потерять последнюю секунду записей: по умолчанию Redis не даёт durability — сохранности записанного — на уровне БД. - Политики вытеснения при исчерпании памяти:
allkeys-lru,volatile-ttl,noevictionи другие. Для кэша обычно LRU/LFU (вытеснение давно или редко используемых ключей) по всем ключам; для очередей и данных, которые нельзя терять, —noevictionи мониторинг. - Масштабирование: реплики для чтения; Redis Cluster шардирует ключи по 16 384 слотам; failover — переключение на реплику при отказе — через Sentinel или Cluster.
Типовые сценарии.
- кэш;
- сессии;
- счётчики и rate limiting (EDGE-06);
- рейтинги на sorted set;
- очереди и Streams;
- lease (аренда) и блокировки — с оговорками из DIST-07;
- pub/sub (издатель — подписчик) для уведомлений внутри системы.
Когда применять. Когда нужны скорость и атомарные операции над структурами. Когда нет: как единственное хранилище важных данных; для больших объёмов холодных данных (память дорога); для сложных запросов и аналитики.
Альтернативы
| Вариант | Когда лучше | Цена |
|---|---|---|
| Redis | Нужны структуры и атомарность | Память, ограниченная durability |
| Memcached | Только простой кэш, максимум простоты | Нет структур, нет персистентности |
| Кэш в памяти процесса | Минимальная задержка, мало данных | N состояний, нет общего доступа |
| БД + индекс | Данные и так помещаются, нагрузка умеренная | Медленнее на порядки |
Trade-offs и режимы отказа. Одна медленная команда блокирует всех: KEYS *, большой SORT, FLUSHALL на большом наборе — типовой способ уронить прод. Большие значения и большие коллекции («big keys») дают всплески задержки. Синхронный fsync на каждую запись резко снижает производительность, а без него теряются данные. Redis Cluster не выполняет операции над ключами из разных слотов — это приходится учитывать в модели данных.
Что мерить.
- задержку команд: p99 и медленные команды через slowlog;
- hit ratio (долю попаданий);
- использование памяти и фрагментацию;
- число вытеснений;
- лаг репликации;
- число подключений.
Связано: CACHE-01 уровни кэша · CACHE-04 патологии · EDGE-06 rate limiting · DIST-07 распределённые блокировки
Источники: Redis — Documentation · Redis — Key eviction · Redis — Persistence · ByteByteGo — Top 5 Redis Use Cases · ByteByteGo — System Design: Why is single-threaded Redis so fast? · ByteByteGo — What Is Redis Really About? Why Is It So Popular?