Кэш (вычисления)
| Назначение | ускорить повторный доступ к данным за счёт их копии в быстром хранилище |
|---|---|
| Метрика | доля попаданий (hit ratio) |
| Политики вытеснения | LRU, LFU, FIFO, TTL, ARC, W-TinyLFU |
| Стратегии записи | write-through, write-back, write-around |
| Уровни | регистры → L1/L2/L3 → ОЗУ → страничный кэш → диск → сеть |
| Прикладные кэши | Redis, Memcached, Varnish, CDN, HTTP-кэш браузера |
Кэш (англ. cache) — небольшое быстрое хранилище, в котором держат копии данных из более медленного источника, чтобы повторные обращения обслуживались быстрее. Кэши есть на всех уровнях вычислительной системы: в процессоре, операционной системе, базах данных, приложениях, браузере и сети доставки контента. Если нужные данные нашлись в кэше — это попадание (cache hit), если нет — промах (cache miss), и данные приходится доставать из источника.
Иерархия памяти
Кэширование работает потому, что программы обладают локальностью: недавно использованные данные скорее всего понадобятся снова (временная локальность), а соседние с ними — тоже (пространственная).[1] Порядки задержек сильно различаются:[7]
| Уровень | Типичная задержка |
|---|---|
| Кэш L1 процессора | ~1 нс |
| Кэш L2 / L3 | ~4 / ~40 нс |
| Оперативная память | ~100 нс |
| Чтение с NVMe SSD | ~20–100 мкс |
| Запрос в пределах дата-центра | ~0,5 мс |
| Запрос через континент | ~50–150 мс |
Процессор читает память строками кэша по 64 байта, поэтому последовательный проход по массиву многократно быстрее «прыжков» по связному списку, хотя асимптотика обоих — O(n) (см. Алгоритм и O-нотация). Операционная система держит недавно прочитанные блоки файлов в страничном кэше — поэтому свободная память в Linux почти всегда «занята» под buff/cache, и это нормально.
Где кэшируют в приложениях
- Мемоизация функций — запоминание результата для одинаковых аргументов:[4]
from functools import lru_cache
@lru_cache(maxsize=1024)
def get_rate(currency: str) -> float:
return fetch_from_api(currency) # медленный внешний вызов
- Кэш перед базой данных — Redis или Memcached хранят результаты частых запросов, сессии, счётчики.
- HTTP-кэш — браузер, обратный прокси и CDN сохраняют ответы по правилам заголовков
Cache-Control,ETag,Last-Modified(RFC 9111).[2][3]
Cache-Control: public, max-age=31536000, immutable # статика с хешем в имени
Cache-Control: no-cache # хранить, но проверять каждый раз
ETag: "33a64df5" # клиент спросит If-None-Match → 304
- Кэши сборки — слои Docker-образов, кэш зависимостей в CI/CD,
__pycache__, кэш компилятора. - Кэш в LLM — KV-кэш внимания при генерации и кэширование промптов у API-провайдеров.
Стратегии чтения и записи
- Cache-aside (ленивая загрузка) — самая распространённая: приложение ищет в кэше, при промахе читает из БД и кладёт результат в кэш с TTL.[6]
def get_user(uid):
if (cached := redis.get(f"user:{uid}")) is not None:
return json.loads(cached)
user = db.fetch_user(uid)
redis.set(f"user:{uid}", json.dumps(user), ex=300) # TTL 5 минут
return user
- Write-through — запись идёт одновременно в кэш и в источник; данные согласованы, запись медленнее.
- Write-back — запись сначала только в кэш, в источник позже; быстро, но при сбое данные могут потеряться (так работают кэши процессора и дисков).
- Write-around — запись мимо кэша, он заполняется только при чтении.
Вытеснение
Кэш ограничен, поэтому при заполнении нужно решать, что выбросить:
- LRU (least recently used) — дольше всего не использовавшийся элемент; классическая реализация — хеш-таблица плюс двусвязный список, обе операции за O(1);
- LFU — используемый реже всего;
- FIFO, случайное вытеснение;
- TTL — элемент истекает по времени;
- адаптивные алгоритмы — ARC, W-TinyLFU (библиотеки Caffeine, Ristretto).
В Redis политика задаётся параметром maxmemory-policy, например allkeys-lru или volatile-ttl.[5]
Инвалидация и типовые проблемы
Известная шутка Фила Карлтона: «В информатике есть только две сложные вещи — инвалидация кэша и придумывание имён». Главные трудности:
- Устаревшие данные — источник изменился, а кэш отдаёт старое. Решения: короткий TTL, явное удаление ключа при изменении, версии в ключах (
user:42:v7), хеш содержимого в имени файла для статики. - Лавина промахов (cache stampede) — популярный ключ истёк, и тысячи запросов одновременно идут в БД. Помогают блокировка на перестроение, ранний случайный перерасчёт и устаревшие ответы на время обновления (
stale-while-revalidate). - Холодный старт — после перезапуска кэш пуст, нагрузка на источник резко растёт.
- Кэширование персональных данных — ответ одного пользователя, ошибочно закэшированный общим кэшем, увидят другие; для таких ответов нужен
Cache-Control: privateилиno-store. - Согласованность между несколькими кэшами и экземплярами приложения — требует распределённых механизмов инвалидации.
Кэш — оптимизация, а не источник истины: система должна работать корректно (пусть и медленнее), если кэш пуст или недоступен.