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

Что такое кэш первого уровня в Hibernate

Кэш первого уровня (L1 cache) — встроенный кэш на уровне EntityManager/Session. Он является фундаментальной частью Hibernate и обеспечивает идентичность сущностей, dirty checkin...

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

Обзор

Кэш первого уровня (L1 cache) — встроенный кэш на уровне EntityManager/Session. Он является фундаментальной частью Hibernate и обеспечивает идентичность сущностей, dirty checking и оптимизацию запросов.

Зачем: L1 cache решает три проблемы: (1) Identity guarantee — один ID = один объект в памяти; (2) Dirty checking — нужно хранить snapshot для сравнения; (3) Оптимизация — повторный find() не делает SELECT.


🟢 Junior Level

Что такое L1 cache

Кэш первого уровня — это кэш внутри EntityManager (сессии). Все загруженные сущности сохраняются в нём автоматически.

// Первый запрос — SELECT из БД
User user1 = entityManager.find(User.class, 1L);
// SQL: SELECT * FROM users WHERE id = 1

// Второй запрос — из кэша, БЕЗ SELECT!
User user2 = entityManager.find(User.class, 1L);
// SQL: (нет запроса!)

assert user1 == user2;  // true — один и тот же объект в памяти

Основные свойства

  • ✅ Включён по умолчанию, нельзя отключить
  • 🔄 Живёт в пределах одной сессии/транзакции
  • 🗑️ Автоматически очищается при закрытии EntityManager
  • 🔒 Обеспечивает identity guarantee (один ID = один объект). Если в одной транзакции дважды загрузить User(1), Hibernate вернёт ОДИН И ТОТ ЖЕ объект в памяти (user1 == user2). Это нужно чтобы dirty checking работал корректно — иначе изменения одного объекта могли бы конфликтовать с другим.

Когда срабатывает

// find() — проверяет кэш перед запросом к БД
User user1 = entityManager.find(User.class, 1L);  // SELECT
User user2 = entityManager.find(User.class, 1L);  // из кэша

// persist() — добавляет в кэш
User newUser = new User();
entityManager.persist(newUser);  // в кэше

// merge() — добавляет обновлённую копию в кэш
User managed = entityManager.merge(detachedUser);  // в кэше

🟡 Middle Level

Когда L1 cache вреден

  1. Загрузка 10k+ сущностей в одной транзакции — OutOfMemoryError
  2. Streaming больших данных — используйте ScrollableResults
  3. Пакетные операции — используйте StatelessSession

Dirty checking через L1 cache

// Hibernate сохраняет snapshot при загрузке
User user = entityManager.find(User.class, 1L);  // snapshot создан

// Изменяем поле
user.setName("New Name");

// При commit/flush — Hibernate сравнивает snapshot с текущим состоянием
// Если разные — автоматически делает UPDATE
// Не нужен явный entityManager.merge()!

Управление кэшем

// Очистить весь кэш
entityManager.clear();
// Все сущности становятся detached

// Удалить конкретную сущность из кэша
entityManager.detach(user);
// user становится detached, остальные остаются

// Перезагрузить из БД (обновить кэш)
entityManager.refresh(user);
// SELECT из БД, обновление snapshot

// Проверить наличие в кэше
boolean isManaged = entityManager.contains(user);

Проблема большого L1 cache

// ❌ Загрузили 100k entities → OutOfMemoryError!
for (Long id : allIds) {
    entityManager.find(User.class, id);  // все в кэше
}

// ✅ Решение: периодическая очистка
for (int i = 0; i < allIds.size(); i++) {
    entityManager.find(User.class, allIds.get(i));

    if (i % 100 == 0) {
        entityManager.flush();   // сбросить в БД
        entityManager.clear();   // очистить кэш
    }
}

Типичные ошибки

// ❌ Долгоживущая транзакция с большим кэшем
@Transactional
public void processAll() {
    List<User> users = userRepository.findAll();  // 50k users
    for (User user : users) {
        process(user);  // L1 cache растёт
    }
    // OutOfMemoryError!
}

// ✅ Решение: пакетная обработка
@Transactional
public void processAll() {
    List<User> users = userRepository.findAll();
    for (int i = 0; i < users.size(); i++) {
        process(users.get(i));
        if (i % 100 == 0) {
            entityManager.flush();
            entityManager.clear();
        }
    }
}

🔴 Senior Level

L1 cache vs ручной Map<String, User>

В отличие от ручного Map, L1 cache: (1) автоматически заполняется при любом запросе; (2) обеспечивает identity guarantee; (3) хранит snapshot для dirty checking; (4) автоматически очищается при закрытии EntityManager.

Внутренняя реализация

StatefulPersistenceContext {
    // Map<EntityKey, Entity> — кэш по ID
    entitiesByKey: Map<EntityKey, Object>

    // Map<Object, EntityEntry> — tracking состояний
    entityEntries: Map<Object, EntityEntry>
}

EntityEntry содержит:
- loadedState — snapshot при загрузке
- status — MANAGED, DELETED, READ_ONLY
- id — identifier
- version — для optimistic locking

Алгоритм find()

entityManager.find(User.class, 1L):

1. Создать EntityKey(User, 1L)
2. Проверить entitiesByKey.containsKey(key)
   → Да: вернуть объект из кэша (БЕЗ SQL)
   → Нет: выполнить SELECT
3. Результат SELECT → создать объект
4. Добавить в entitiesByKey
5. Создать EntityEntry с snapshot
6. Вернуть объект

Read-only hint для оптимизации

// Для сущностей, которые не будут изменяться
List<User> users = entityManager.createQuery("FROM User", User.class)
    .setHint("org.hibernate.readOnly", true)
    .getResultList();

// Если вы измените read-only entity, dirty checking НЕ обнаружит изменений
// и UPDATE не будет выполнен. Hibernate просто проигнорирует изменения.

// Преимущества:
// 1. Не создаётся snapshot (экономия памяти)
// 2. Dirty checking не выполняется
// 3. Меньше overhead на flush

Performance характеристики

L1 Cache overhead:
- O(1) для find() (HashMap lookup)
- O(N) для dirty checking (обход всех entities)
- O(N) памяти (хранение snapshot для каждого entity)

Для больших операций:
- flush() + clear() каждые 50-100 entities
- read-only hint когда не нужно изменение
- StatelessSession для массовых операций

Best Practices

✅ L1 cache работает по умолчанию
✅ entityManager.clear() для пакетных операций
✅ entityManager.detach() для отдельных объектов
✅ refresh() для обновления из БД
✅ read-only hint для запросов без изменений
✅ flush + clear каждые 50-100 entities в batch

❌ Загрузка большого количества entities без clear
❌ Игнорирование OutOfMemoryError при больших операциях
❌ Долгоживущие транзакции с большим кэшем
❌ Использование L1 cache как кэш между запросами

🎯 Шпаргалка для интервью

Обязательно знать:

  • L1 cache — встроенный кэш EntityManager, включён по умолчанию, нельзя отключить
  • Решает 3 проблемы: identity guarantee, dirty checking (snapshot), оптимизация запросов
  • find() проверяет кэш перед запросом к БД — повторный find() без SQL
  • Живёт в пределах одной транзакции, очищается при закрытии EntityManager
  • При загрузке 10k+ сущностей — OutOfMemoryError, нужен clear()
  • Read-only hint экономит память (не создаёт snapshot)

Частые уточняющие вопросы:

  • Можно ли отключить L1 cache? Нет, это фундаментальная часть Hibernate
  • Почему repeatable find() не делает SELECT? Объект уже в entitiesByKey Map
  • Как избежать OOM при batch? flush() + clear() каждые 50-100 entities
  • L1 cache vs ручной Map? L1 cache автоматически заполняется, обеспечивает identity guarantee и хранит snapshot

Красные флаги (НЕ говорить):

  • «Использую L1 cache как кэш между запросами» — он живёт только в рамках одной сессии
  • «Отключаю L1 cache для производительности» — нельзя отключить
  • «Загружаю 100k entities без clear» — OutOfMemoryError
  • «L1 cache заменяет L2 cache» — это разные уровни с разными целями

Связанные темы:

  • [[10. Что такое кэш второго уровня и когда его использовать]]
  • [[11. Как настроить кэш второго уровня]]
  • [[12. Что такое dirty checking в Hibernate]]
  • [[13. Как работает механизм flush в Hibernate]]