Вопрос 10 · Раздел 16

Что такое кэш второго уровня и когда его использовать

Кэш второго уровня (L2 cache) — это кэш на уровне EntityManagerFactory, который разделяется между всеми сессиями. В отличие от L1 cache, он переживает закрытие сессий и может ис...

Версии по языкам: English Russian Ukrainian

Обзор

Кэш второго уровня (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]]