Gymterview
senior

Что такое многоуровневое кэширование (L1 + L2)?

Многоуровневое (multi-level) кэширование — это архитектурный подход с использованием нескольких уровней кэша с разной скоростью и ёмкостью, где типичная схема включает L1 (in-process, Caffeine) и L2 (distributed, Redis).

Аналогия из жизни: L1 + L2 кэш — это как карманный блокнот (L1) и общий справочник в офисе (L2). В блокноте мало места, но он всегда под рукой. Справочник общий для всех сотрудников и содержит больше информации, но до него нужно дойти.

Схема работы

Пример
Запрос → L1 (Caffeine, <1мс) → L2 (Redis, ~1мс) → БД (~5мс)

Реализация через CompositeCacheManager

Пример кода
@Configuration
@EnableCaching
public class MultiLevelCacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisFactory) {
        // L1 — Caffeine (in-process)
        CaffeineCacheManager caffeineManager = new CaffeineCacheManager();
        caffeineManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1_000)
            .expireAfterWrite(Duration.ofMinutes(5))); // короткий TTL для L1

        // L2 — Redis (distributed)
        RedisCacheManager redisManager = RedisCacheManager.builder(redisFactory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))) // длинный TTL для L2
            .build();

        // Композитный CacheManager
        return new CompositeCacheManager(caffeineManager, redisManager);
    }
}

Кастомная реализация с явным управлением

Пример кода
@Service
public class UserService {
    private final LoadingCache<Long, User> l1Cache;  // Caffeine
    private final RedisTemplate<String, User> redis;  // Redis
    private final UserRepository repository;

    public User findById(Long id) {
        return l1Cache.get(id, key -> {
            // L1 miss → проверяем L2
            User fromRedis = redis.opsForValue().get("user:" + key);
            if (fromRedis != null) return fromRedis;

            // L2 miss → БД
            User fromDb = repository.findById(key).orElseThrow();
            redis.opsForValue().set("user:" + key, fromDb, Duration.ofMinutes(30));
            return fromDb;
        });
    }

    public void evict(Long id) {
        l1Cache.invalidate(id);               // инвалидировать L1
        redis.delete("user:" + id);           // инвалидировать L2
        // + Redis Pub/Sub для инвалидации L1 на других экземплярах
    }
}

Сравнение уровней

Характеристика L1 (Caffeine) L2 (Redis)
Latency <1 мс ~1 мс
Расположение В heap JVM процесса Отдельный сервер
Видимость Только текущий экземпляр Все экземпляры
Ёмкость Ограничена heap Ограничена RAM сервера Redis
TTL Короткий (1-5 мин) Длинный (10-60 мин)

Ключевые принципы

  • L1 (Caffeine) — менее 1 мс, ограничен размером heap; L2 (Redis) — около 1 мс, разделяемый между экземплярами
  • L1 TTL < L2 TTL — L1 обновляется чаще, чтобы не расходиться с L2
  • Инвалидация L1 при обновлении данных — нужен Redis Pub/Sub или Kafka для уведомления всех экземпляров

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

  • L1 без инвалидации между экземплярами — экземпляр A обновил данные, экземпляр B отдаёт старые из L1
  • Одинаковый TTL для L1 и L2 — L1 должен быть короче, чтобы периодически подтягивать из L2
  • Слишком большой L1 — Caffeine в heap; при 100K объектов x 1KB = 100MB heap

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

  • Caffeine (L1) + Redis (L2) — стандартная комбинация для high-traffic приложений
  • Библиотеки: caffeine-redis-cache, spring-boot-starter-cache с кастомным CacheManager
  • В Spring Boot нет multi-level из коробки — нужна кастомная конфигурация или сторонняя библиотека

На собеседовании: интервьюер проверяет понимание зачем нужны два уровня и как решается проблема инвалидации L1 в кластере. Частая ошибка — не упомянуть Pub/Sub для синхронизации L1-кэшей между экземплярами.