Gymterview
middle

Какие паттерны кэширования существуют?

Паттерны кэширования — это устоявшиеся стратегии взаимодействия приложения, кэша и источника данных, определяющие кто, когда и как читает и записывает данные.

Cache-Aside (Lazy Loading)

Самый распространённый паттерн. Приложение управляет кэшем явно: сначала проверяет кэш, при промахе загружает из БД и кладёт в кэш.

Пример кода
public User getUser(Long id) {
    // 1. Проверить кэш
    User cached = cache.get("user:" + id);
    if (cached != null) return cached;

    // 2. Cache miss — загрузить из БД
    User user = userRepository.findById(id).orElseThrow();

    // 3. Положить в кэш
    cache.put("user:" + id, user, Duration.ofMinutes(10));
    return user;
}
// Spring Cache реализует Cache-Aside автоматически
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
    return userRepository.findById(id).orElseThrow();
}

Плюсы: кэшируются только запрошенные данные; промах заполняет кэш. Минусы: первый запрос всегда медленный (cold start); возможна несогласованность при обновлении БД без инвалидации кэша.

Read-Through

Кэш сам загружает данные из источника при промахе. Приложение всегда обращается к кэшу, не к БД напрямую.

Пример кода
// Caffeine с CacheLoader — Read-Through
LoadingCache<Long, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .build(id -> userRepository.findById(id).orElseThrow()); // CacheLoader

// Использование — кэш сам вызовет loader при промахе
User user = cache.get(userId);

Отличие от Cache-Aside: логика загрузки инкапсулирована в кэше, а не в сервисе.

Write-Through

При записи данные одновременно пишутся и в кэш, и в БД. Кэш всегда содержит актуальные данные.

Пример кода
public User updateUser(Long id, UpdateRequest request) {
    User user = userRepository.save(toEntity(request)); // запись в БД
    cache.put("user:" + id, user);                      // запись в кэш
    return user;
}
// Spring Cache — @CachePut реализует Write-Through
@CachePut(value = "users", key = "#result.id")
public User updateUser(UpdateRequest request) {
    return userRepository.save(toEntity(request));
}

Плюсы: кэш всегда актуален. Минусы: каждая запись медленнее (двойная запись); кэшируются данные, которые могут никогда не быть прочитаны.

Write-Behind (Write-Back)

Данные сначала пишутся в кэш, а запись в БД происходит асинхронно (с задержкой или батчами).

Пример
Приложение → Кэш (мгновенно) → [асинхронно, батчами] → БД

Плюсы: максимальная скорость записи; можно агрегировать множество записей в один batch. Минусы: риск потери данных при сбое кэша до синхронизации с БД. Применение: счётчики, метрики, аналитика, write-heavy нагрузка.

Refresh-Ahead

Кэш проактивно обновляет данные до истечения TTL для горячих ключей.

Пример
// Caffeine с refreshAfterWrite — автоматическое фоновое обновление
LoadingCache<Long, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(30))     // жёсткое удаление через 30 мин
    .refreshAfterWrite(Duration.ofMinutes(10))    // фоновое обновление через 10 мин
    .build(id -> userRepository.findById(id).orElseThrow());
// Через 10 мин: возвращает старое значение + запускает фоновую загрузку нового
// Через 30 мин: ключ удаляется, следующий запрос — полный cache miss

Плюсы: нет холодных промахов для горячих данных. Минусы: сложнее в реализации; нагрузка на БД для обновления даже незапрашиваемых данных.

Сравнение паттернов

Паттерн Кто читает из БД Кто пишет в БД Consistency Latency чтения Latency записи
Cache-Aside Приложение Приложение Eventual Miss — медленно Без кэша
Read-Through Кэш Приложение Eventual Miss — медленно Без кэша
Write-Through Кэш Кэш + БД синхронно Strong Hit — быстро Медленнее (2 записи)
Write-Behind Кэш Кэш, затем БД асинхронно Eventual Быстро Быстро
Refresh-Ahead Кэш (проактивно) Приложение Eventual Всегда быстро Без кэша

Выводы

  • Cache-Aside + TTL — самый распространённый и простой паттерн; Spring @Cacheable реализует его
  • Write-Through — когда нужна согласованность кэша с БД; Spring @CachePut
  • Write-Behind — для write-heavy нагрузок (счётчики, логи)
  • Refresh-Ahead — для данных, которые должны быть всегда теплыми (каталог товаров, курсы валют)

Частые ошибки

  • Cache-Aside без инвалидации — обновили БД, забыли обновить/удалить кэш, получили stale data
  • Write-Through для редко читаемых данных — кэш заполняется данными, которые никто не читает
  • Write-Behind без обработки ошибок — если асинхронная запись в БД упала, данные потеряны
  • Путать паттерны — Cache-Aside + @CachePut = рабочая комбинация; Read-Through в Spring — через Caffeine LoadingCache

Как используется в 2026

  • Cache-Aside — 80%+ всех кэширований в Spring-приложениях
  • Write-Behind — используется в Redis Streams и Apache Kafka для буферизации записей
  • Caffeine refreshAfterWrite — стандарт для проактивного обновления справочных данных

На собеседовании: интервьюер проверяет, понимаете ли вы разницу между паттернами и умеете ли выбрать нужный под задачу. Частая ошибка — описать только Cache-Aside и не знать Write-Behind и Refresh-Ahead.