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-кэшей между экземплярами.