Современные приложения обрабатывают гигабайты данных при миллионах одновременных запросов. Когда база данных становится узким горлышком системы, первое, о чем вспоминают инженеры — кэширование. Но превратить эту концепцию в эффективную архитектурную практику сложнее, чем кажется.
Механизмы кэширования: от базы данных до CDN
В стеке современных приложений кэши работает на трех ключевых уровнях:
-
Database Caching
Встроенные механизмы вроде буфера запросов MySQL или кэша коллекций MongoDB. Пример: подсистема InnoDB кэширует 80% часто читаемых страниц данных по умолчанию. Ошибка: попытка переложить всё кэширование на СУБД при репликации с задержкой. -
Application-Level Caching
Redis/Memcached перед реляционной базой сокращают нагрузку на запросы типа:
SELECT * FROM products WHERE category_id = 5 ORDER BY created_at DESC LIMIT 20
Но код должен обрабатывать согласованность:
def get_products(category_id):
cache_key = f"products:{category_id}"
data = redis.get(cache_key)
if not data:
data = db.query(...).serialize()
redis.setex(cache_key, TTL, data) # TTL=300?
return deserialize(data)
- CDN для статики и динамики
Cloudflare Workers или VCL Varnish конфигурируют кэширование HTTP-ответов. Сервис рекомендаций может отдавать JSON сCache-Control: public, max-age=120, s-maxage=60
.
Паттерны: не только Cache-Aside
Write-Through vs Write-Behind
При частых обновлениях данных:
- Write-Through синхронно обновляет кэш и БД. Консистентность ценой задержки записи.
func UpdateProduct(product Product) error {
err := db.Update(product)
if err == nil {
cache.Set(product.ID, product, 0)
}
return err
}
- Write-Behind (через брокеры вроде Kafka) обеспечивает асинхронную запись через очередь. Риски: возможная потеря данных при сбое.
Шаблон Read-Through полезен при сложных запросах:
Client → Cache → [Cache Miss → Loader → DB → Populate Cache] → Response
Но требует интеллектуального кэш-провайдера с поддержкой ленивой загрузки.
Проблемы инвалидации и консистентности
Сценарий: пользователь редактирует профиль, но в течение TTL=300s кэш продолжает отдавать старые данные. Решения:
- Инвалидация по событию через брокер сообщений:
messageBus.subscribe('user_updated', (event) => {
cache.del(`user:${event.userId}`);
});
- Условные запросы с ETag при частичных обновлениях.
Опасность dog-pile (cache stampede): 1000 одновременных запросов к устаревшему ключу. Техники смягчения:
- Реакционные locks:
if !cache.exist?(key)
lock = acquire_lock(key)
if lock
data = db.query(...)
cache.write(key, data)
release_lock(key)
else
sleep(rand(50..200))
retry
end
end
- Вероятностное продление TTL: добавить случайные 10% к базовому значению.
Вытестнение по LRU — только вершина айсберга
Redis предлагает 7 политик, включая LFU (Least Frequently Used). При выборе учитывают шаблон доступа:
- Volatile-LRU для данных с естественным TTL
- Allkeys-LFU когда 20% ключей составляют 80% запросов
Метрики для настройки: hit rate (цель >95%), среднее время загрузки кэш-промаха, распределение размеров объектов.
Антипаттерны: когда кэши мешают
-
Кэширование уникальных запросов (OFFSET 1000 в постраничке) — ведет к раздуванию памяти без повышения hit rate.
-
Игнорирование сериализции — JSON.parse(JSON.stringify(data)) вдвое медленнее MessagePack или Protocol Buffers.
-
Слепое доверие TTL — страницы товаров могут требовать инвалидации при изменении цены, а не по таймеру.
Заключение: измеряйте перед оптимизацией
Прежде чем добавлять кэш-слой, добавьте метрики:
- Топ-10 самых медленных запросов к БД
- Соотношение чтений/записей для горячих сущностей
- Процент идентичных запросов в логах API
Инструменты: запросы EXPLAIN ANALYZE
, Redis командой INFO stats
, распределенные трейсеры вроде Jaeger. Кэш — не серебряная пуля, но при точной настройке превращает узкое горлышко в конкурентное преимущество.