Что такое кэш первого уровня в Hibernate
Кэш первого уровня (L1 cache) — встроенный кэш на уровне EntityManager/Session. Он является фундаментальной частью Hibernate и обеспечивает идентичность сущностей, dirty checkin...
Обзор
Кэш первого уровня (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 вреден
- Загрузка 10k+ сущностей в одной транзакции — OutOfMemoryError
- Streaming больших данных — используйте ScrollableResults
- Пакетные операции — используйте 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]]