Какие стратегии кэширования существуют?
Кэширование – хранение часто запрашиваемых данных в быстром хранилище для снижения нагрузки на основной источник данных и уменьшения времени отклика. Как шпаргалка, в которую заглядываешь вместо того, чтобы каждый раз открывать учебник.
1. Cache-Aside (Lazy Loading)
Приложение само управляет кэшем. При промахе читает из БД и кладёт в кэш.
Пример
Запрос → Есть в кэше?
├── Да → Вернуть из кэша
└── Нет → Прочитать из БД → Записать в кэш → Вернуть
Пример кода
@Service
public class AccountService {
private final AccountRepository repository;
private final RedisTemplate<String, Account> redisTemplate;
public Account findById(Long id) {
String key = "account:" + id;
Account cached = redisTemplate.opsForValue().get(key);
if (cached != null) return cached;
Account account = repository.findById(id)
.orElseThrow(() -> new NotFoundException(id));
redisTemplate.opsForValue().set(key, account, Duration.ofMinutes(30));
return account;
}
}
Плюс: кэшируются только реально запрашиваемые данные. Минус: первый запрос всегда медленный (cache miss); данные могут устаревать до истечения TTL.
2. Write-Through
При записи данные сохраняются одновременно в кэш и в БД.
Пример
Запись → Обновить кэш → Обновить БД → Ответ
Плюс: кэш всегда актуален. Минус: увеличенная задержка записи; кэшируются данные, которые могут никогда не читаться.
3. Write-Behind (Write-Back)
Данные записываются в кэш немедленно, а в БД – асинхронно с задержкой.
Пример
Запись → Обновить кэш → Ответ
│
└── Асинхронно (через N мс) → Обновить БД
Плюс: очень быстрая запись. Минус: риск потери данных при сбое кэша до синхронизации с БД. Этот подход не подходит для финансовых операций, где потеря данных недопустима.
4. Read-Through
Кэш сам обращается к БД при промахе (в отличие от Cache-Aside, где это делает приложение). Приложение всегда обращается к кэшу и не знает об источнике данных.
5. Refresh-Ahead
Кэш обновляет данные до истечения TTL, если они часто запрашиваются. Предотвращает cache miss для горячих данных.
Сравнение стратегий
| Стратегия | Кто читает из БД | Кто пишет в БД | Консистентность | Задержка записи |
|---|---|---|---|---|
| Cache-Aside | Приложение | Приложение | Возможны stale data | Нет (пишет напрямую) |
| Write-Through | Кэш | Кэш (синхронно) | Высокая | Увеличена |
| Write-Behind | Кэш | Кэш (асинхронно) | Возможна потеря | Минимальная |
| Read-Through | Кэш | Приложение | Возможны stale data | Нет |
| Refresh-Ahead | Кэш (проактивно) | Приложение | Высокая для hot data | Нет |
Инструменты кэширования в Java
| Инструмент | Тип | Применение |
|---|---|---|
| Redis | Распределённый | Кэш между микросервисами, сессии, лимиты |
| EhCache | Локальный (in-process) | Кэш внутри одного приложения |
| Caffeine | Локальный (in-process) | Быстрый кэш в памяти JVM, рекомендуется для новых проектов |
| Hazelcast | Распределённый | Distributed cache, in-memory data grid |
Использование Spring Cache
Spring предоставляет абстракцию кэширования через аннотации, скрывая конкретную реализацию:
Пример кода
@Service
public class ClientService {
@Cacheable(value = "clients", key = "#id")
public ClientDto findById(Long id) {
return clientRepository.findById(id)
.map(ClientMapper::toDto)
.orElseThrow();
}
@CacheEvict(value = "clients", key = "#id")
public void updateClient(Long id, UpdateClientRequest request) {
// обновление клиента в БД
}
@CacheEvict(value = "clients", allEntries = true)
public void clearCache() { }
}
Типичные проблемы кэширования
- Cache Stampede (thundering herd) – множество одновременных запросов при промахе кэша, все идут в БД. Решение: блокировка (mutex) при подгрузке или вероятностное раннее обновление.
- Stale data – устаревшие данные. Решение: TTL, активная инвалидация при обновлении.
- Cache Invalidation – одна из двух самых сложных проблем в computer science (Phil Karlton). Особенно сложна в распределённых системах с несколькими инстансами приложения.
На собеседовании: Интервьюер хочет услышать не просто перечисление стратегий, а понимание trade-off каждой. Частая ошибка – не упоминать проблемы инвалидации и Cache Stampede.