Кеш хранит результат дорогой операции ближе к потребителю. Чтение из Redis занимает около миллисекунды, а тяжёлый запрос в БД может занимать десятки и сотни. Кеш выгоден, когда данные читают намного чаще, чем меняют.
Где бывает кеш
- Браузер: HTTP-заголовки
Cache-Control,ETag. - CDN: статика, картинки, иногда целые страницы.
- Обратный прокси: nginx кеширует ответы бэкенда.
- В процессе приложения: самый быстрый, но у каждого экземпляра свой и они расходятся.
- Распределённый кеш: Redis, Memcached. Общий для всех экземпляров.
- Внутри БД: буферный кеш PostgreSQL (shared_buffers) и кеш ОС.
Стратегии чтения и записи
- Cache-aside (lazy loading). Приложение читает кеш; при промахе идёт в БД и кладёт результат в кеш с TTL. Самая распространённая схема. При записи данные пишут в БД и удаляют ключ из кеша.
- Read-through. То же, но в кеш ходит библиотека или сам кеш-слой.
- Write-through. Запись идёт в кеш и синхронно в БД. Кеш всегда свежий, запись медленнее.
- Write-behind (write-back). Запись в кеш, в БД асинхронно пачками. Быстро, но при падении кеша можно потерять данные.
Почему при обновлении ключ лучше удалить, а не перезаписать: два параллельных обновления могут записать в кеш значения в другом порядке, чем в БД, и кеш останется со старым значением надолго. Удаление безопаснее, хотя и оно не идеально, поэтому всегда ставят TTL как страховку.
Инвалидация
Классическая шутка: в программировании две сложные вещи, инвалидация кеша и именование. Варианты:
- TTL: просто и надёжно, но данные могут быть устаревшими до истечения TTL.
- Явное удаление при записи.
- События изменений (CDC из БД через Debezium в Kafka), подписчики чистят кеш.
- Версионирование ключей:
product:42:v7, при изменении версия растёт.
Вытеснение
Когда память кончается, кеш выкидывает записи по политике: LRU (давно не использованные), LFU (редко используемые), FIFO, случайно. В Redis задаётся maxmemory-policy, например allkeys-lru или allkeys-lfu.
Типичные проблемы
- Cache stampede (лавина, dogpile). Популярный ключ истёк, и тысячи запросов одновременно идут в БД. Решения: блокировка на перестроение (только один запрос идёт в БД), ранее обновление до истечения, отдача устаревшего значения пока строится новое.
- Массовое истечение. Много ключей поставили с одинаковым TTL, и они истекли разом. Решение: добавлять к TTL случайный разброс (jitter).
- Cache penetration. Запросы к несуществующим ключам всегда идут в БД. Решение: кешировать пустой результат с коротким TTL или фильтр Блума.
- Горячий ключ. Один ключ получает огромную долю трафика и перегружает один узел Redis. Решение: локальный кеш в процессе на секунды, реплики ключа с суффиксом.
Hit rate
Доля попаданий в кеш. При hit rate 90% в БД доходит 10% чтений, при 99% только 1%. Рост с 90 до 99 процентов снижает нагрузку на БД в 10 раз.
Частые ошибки
- Кешировать всё подряд без понимания паттерна доступа.
- Не ставить TTL.
- Считать кеш источником истины без персистентности.
- Забыть, что после рестарта кеш холодный и вся нагрузка пойдёт в БД.
Что спросят на собесе
- Какие стратегии кеширования знаете, чем cache-aside отличается от write-through.
- Как инвалидировать кеш при изменении данных.
- Что такое cache stampede и как с ним бороться.
- Где в системе можно поставить кеш.
- Что делать с горячим ключом.