Что такое кэш второго уровня и когда его использовать
Кэш второго уровня (L2 cache) — это кэш на уровне EntityManagerFactory, который разделяется между всеми сессиями. В отличие от L1 cache, он переживает закрытие сессий и может ис...
Обзор
Кэш второго уровня (L2 cache) — это кэш на уровне EntityManagerFactory, который разделяется между всеми сессиями. В отличие от L1 cache, он переживает закрытие сессий и может использоваться несколькими транзакциями.
Зачем: когда 100 пользователей одновременно запрашивают один и тот же справочник (например, список стран), L1 cache бесполезен — у каждой сессии свой кэш. L2 cache загрузит страны ОДИН раз и поделит между всеми.
🟢 Junior Level
Что такое L2 cache
Кэш второго уровня — это кэш, который разделяется между сессиями. Если одна сессия загрузила сущность, другая сессия может взять её из L2 cache без обращения к БД.
Сессия 1: загрует User(1) → L1 cache + L2 cache
Сессия 1: закрыта → L1 cache удалён, L2 cache остался
Сессия 2: загрует User(1) → L2 cache (БЕЗ БД!)
L1 vs L2 cache
| Характеристика | L1 Cache | L2 Cache |
|---|---|---|
| Уровень | EntityManager | EntityManagerFactory |
| По умолчанию | ✅ Включён | ❌ Нужно включить |
| Жизненный цикл | Одна сессия | Всё приложение |
| Разделение | Нет | Между сессиями |
| Провайдер | Встроен | Ehcache, Hazelcast, Redis |
Когда L2 cache полезен
✅ Подходит когда:
- Данные редко меняются (справочники, страны, валюты)
- Много чтений, мало записей
- Несколько экземпляров приложения
❌ Не подходит когда:
- Данные часто меняются (заказы, логи)
- Нужна абсолютная актуальность данных
- Больше записей чем чтений
🟡 Middle Level
Когда НЕ использовать L2 cache
L2 cache НЕ работает с:
- Native SQL queries (Hibernate не знает какие сущности затронуты)
- Query cache для пагинации (кешируется отдельно)
- EntityManager.clear() не чистит L2 cache
Конфигурация
<!-- Зависимость для JCache провайдера -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
# application.yml
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region:
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
javax.persistence.sharedCache.mode: ENABLE_SELECTIVE
В Hibernate 5.x используется javax.persistence. В Hibernate 6+ / Jakarta EE замените на jakarta.persistence и уточните пути к factory classes.
// Пометка сущности для кэширования
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
@Id
private Long id;
private String name;
}
Стратегии кэширования
READ_ONLY:
- Только чтение, данные никогда не меняются
- Самая быстрая стратегия
- Для справочников, стран, валют
READ_WRITE:
- Чтение и запись с синхронизацией
- Использует soft locks
- Для умеренно изменяемых данных
NONSTRICT_READ_WRITE:
- Чтение и запись без строгой синхронизации
- Может вернуть устаревшие данные
- Для редко изменяемых данных
TRANSACTIONAL:
- Полная транзакционная согласованность
- Требует XA транзакций
- Для критически важных данных
Управление кэшем
Cache cache = entityManager.getEntityManagerFactory().getCache();
// Evict конкретную сущность
cache.evict(User.class, userId);
// Evict все сущности типа
cache.evict(User.class);
// Evict всё
cache.evictAll();
Типичные ошибки
// ❌ Кэш для часто меняющихся данных
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Order { }
// Orders часто меняются → устаревшие данные!
// ❌ Нет TTL
// Кэш растёт бесконечно → memory leak
// Решение: всегда задавайте TTL
🔴 Senior Level
Soft locks — мягкая блокировка: Hibernate помечает запись в кэше как «обновляется», другие потоки ждут. В отличие от жёсткой блокировки БД, это не блокирует строку в таблице.
XA транзакции — двухфазные транзакции, обеспечивающие атомарность операций и в БД, и в кэше.
Eventual consistency — согласованность в конечном счёте: после обновления данные в кэше на разных нодах станут одинаковыми не мгновенно, а через некоторое время.
Invalidation механизм
L2 cache invalidation происходит при:
- UPDATE/DELETE сущности → eviction из кэша
- Явный evict() → удаление из кэша
- TTL expiration → автоматическое удаление
- Cluster replication → репликация между нодами
Кластерный кэш
# Hazelcast L2 cache для кластера
spring:
jpa:
properties:
hibernate:
cache:
region:
factory_class: com.hazelcast.hibernate.HazelcastCacheRegionFactory
Для нескольких инстансов приложения:
- Репликация кэша между нодами (Hazelcast, Redis)
- Консистентность eventual (не strong)
- Network overhead при репликации
- Нужно учитывать при проектировании
При обновлении User на Node 1: (1) Hibernate обновляет БД;
(2) invalidates запись в L2 cache Node 1;
(3) Hazelcast рассылает invalidation message на Node 2, 3;
(4) Next запрос на Node 2 промахивается в кэш и загружает из БД.
Production мониторинка
// Hibernate Statistics
Statistics stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
// L2 cache метрики
long hitCount = stats.getSecondLevelCacheHitCount();
long missCount = stats.getSecondLevelCacheMissCount();
double hitRatio = (double) hitCount / (hitCount + missCount);
// Для production мониторинга
// hitRatio > 0.8 — отлично, кэш эффективно используется
// hitRatio < 0.5 — проверьте: правильно ли выбраны сущности для кэширования, достаточен ли размер кэша, не слишком ли мал TTL
Настройка Ehcache
<!-- ehcache.xml -->
<config>
<cache alias="com.example.User">
<heap unit="entries">1000</heap>
<offheap unit="MB">50</offheap>
<expiry>
<ttl unit="minutes">30</ttl>
</expiry>
</cache>
<cache alias="com.example.Country">
<heap unit="entries">500</heap>
<expiry>
<ttl unit="hours">24</ttl>
</expiry>
</cache>
</config>
Query cache vs L2 cache
Query cache хранит: «запрос X с параметрами Y вернул сущности с ID [1,2,3]». L2 cache хранит: «сущность User(1) = {name: John, …}». Они работают вместе: Query cache даёт ID, L2 cache даёт данные. Query cache бесполезен без L2 cache.
Best Practices
✅ READ_ONLY для справочников и констант
✅ READ_WRITE для умеренно изменяемых данных
✅ Мониторьте hit/miss ratio
✅ Задавайте TTL для каждого region
✅ Ограничивайте heap size
✅ Invalidация при обновлениях
✅ Кластерный кэш для multi-node
❌ Для часто меняющихся данных
❌ Без мониторинга hit ratio
❌ Без TTL и лимитов памяти
❌ Кэш для сущностей с N+1 проблемой
❌ Использование как замена нормальной архитектуры
🎯 Шпаргалка для интервью
Обязательно знать:
- L2 cache — на уровне EntityManagerFactory, разделяется между сессиями
- Нужно включить: выбрать провайдера (Ehcache, Hazelcast, Redis), настроить конфигурацию
- Стратегии: READ_ONLY (справочники), READ_WRITE (умеренно изменяемые), NONSTRICT_READ_WRITE, TRANSACTIONAL
- Полезен для: редко меняющих данных, много чтений/мало записей, справочников
- Query cache хранит ID сущностей, сами данные — из L2 cache (бесполезен без L2)
- Мониторинг: hitRatio > 0.8 отлично, < 0.5 — проверить конфигурацию
Частые уточняющие вопросы:
- L1 vs L2 cache? L1 — в рамках сессии (всегда включён), L2 — между сессиями (нужно включить)
- Когда L2 cache НЕ работает? Native SQL queries, query cache для пагинации, после EntityManager.clear()
- Как работает кластерный кэш? Репликация между нодами (Hazelcast, Redis), eventual consistency
- Что такое soft locks? Мягкая блокировка — Hibernate помечает запись как «обновляется», другие потоки ждут
Красные флаги (НЕ говорить):
- «Кэширую все сущности подряд» — нужно кэшировать только read-heavy
- «Без TTL — кэш растёт бесконечно» — memory leak
- «L2 cache для Order/Log» — часто меняющиеся данные, кэш неэффективен
- «Query cache без L2 cache» — бесполезен, хранит только ID
Связанные темы:
- [[9. Что такое кэш первого уровня в Hibernate]]
- [[11. Как настроить кэш второго уровня]]
- [[1. Что такое проблема N+1 и как её решить]]
- [[13. Как работает механизм flush в Hibernate]]