Какие уровни кэширования знаешь в Hibernate
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Уровни кэширования в Hibernate
Hibernate предоставляет многоуровневую стратегию кэширования для оптимизации работы с БД. Это критично для performance приложений с интенсивным использованием БД.
1. First-Level Cache (L1) - Кэш сессии
First-level cache - это кэш на уровне Hibernate Session. Он существует только в рамках одной сессии.
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory;
public void demonstrateL1Cache() {
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
// Первый запрос - идёт в БД
User user1 = session.get(User.class, 1L); // SELECT в БД
System.out.println("First query: " + user1.getName());
// Второй запрос - берётся из L1 кэша!
User user2 = session.get(User.class, 1L); // НЕТУ SQL запроса!
System.out.println("Second query: " + user2.getName());
// Это один и тот же объект
System.out.println(user1 == user2); // true - из кэша
tx.commit();
session.close(); // L1 кэш удаляется
}
}
Как работает L1 cache:
Первый запрос:
session.get(User.class, 1)
↓
Есть в L1 кэше? НЕТ
↓
Идём в БД
↓
Результат кэшируем в L1
↓
Возвращаем пользователю
Второй запрос (того же объекта):
session.get(User.class, 1)
↓
Есть в L1 кэше? ДА!
↓
Возвращаем из L1 (NO SQL!)
Характеристики L1:
✅ Всегда включён - нельзя выключить
✅ Автоматический - Hibernate сам управляет
✅ Быстрый - в памяти приложения
✅ Гарантирует идентичность объектов - session.get() всегда вернёт тот же объект
❌ Ограничен на одну сессию - теряется при закрытии session
❌ Может быть большим - если session долгоживущая, объекты накопятся
Управление L1 кэшем:
public void manageL1Cache() {
Session session = sessionFactory.openSession();
User user1 = session.get(User.class, 1L); // В кэше
// Явно удалить из кэша
session.evict(user1); // Удаляем user1 из L1
User user2 = session.get(User.class, 1L); // Снова идём в БД
// Очистить весь L1 кэш
session.clear(); // Удаляем ВСЁ из L1
session.close();
}
2. Second-Level Cache (L2) - Кэш на уровне SessionFactory
Second-level cache - это глобальный кэш для всего приложения. Он живёт дольше одной сессии.
# application.properties
# Включаем L2 кэш
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
# Используем Redis (или другой provider)
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
# Конфигурация TTL
spring.jpa.properties.hibernate.cache.default_cache_concurrency_strategy=read-write
spring.jpa.properties.hibernate.cache.region_prefix=hibernate_cache
Аннотация для Entity:
@Entity
@Table(name = "users")
@Cacheable // Включаем L2 кэширование
@org.hibernate.annotations.Cache(
usage = CacheConcurrencyStrategy.READ_WRITE // Стратегия конкурентности
)
public class User {
@Id
private Long id;
private String email;
private String name;
}
Использование L2 кэша:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void demonstrateL2Cache() {
// Первая сессия
User user1 = userRepository.findById(1L).orElse(null); // SELECT в БД
System.out.println("Session 1: " + user1.getName());
// Сессия закрывается, объект выгружается из L1
// Вторая сессия (другой поток)
User user2 = userRepository.findById(1L).orElse(null); // БЕЗ SQL!
System.out.println("Session 2: " + user2.getName()); // Из L2 кэша
// Объекты разные (user1 != user2) но с одинаковыми данными
}
}
Как работает L2 cache:
Первая сессия:
session1.get(User.class, 1)
↓
Нет в L1? Нет в L2? Идём в БД
↓
Кэшируем в L2 (глобально)
↓
Кэшируем в L1 (session1)
↓
session1.close() - удаляем из L1, но L2 остаётся
Вторая сессия:
session2.get(User.class, 1)
↓
Нет в L1? Проверяем L2?
↓
Есть в L2! Берём оттуда
↓
Кэшируем в L1 (session2)
↓
Возвращаем (NO SQL!)
Стратегии конкурентности L2:
// READ_ONLY - только для чтения
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Country { // Справочник, не меняется
private String name;
}
// NONSTRICT_READ_WRITE - слабая консистентность
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Product { // Редко меняется
private String name;
private BigDecimal price;
}
// READ_WRITE - полная консистентность
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User { // Часто меняется
private String email;
private String name;
}
// TRANSACTIONAL - для JTA транзакций
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class Order { // Критичные данные
private BigDecimal total;
}
Преимущества L2:
✅ Глобальный кэш - используется между сессиями
✅ Снижает нагрузку на БД - меньше SQL запросов
✅ Делится между потоками - одни данные для всех
Недостатки L2:
❌ Проблемы с консистентностью - если БД изменится напрямую (не через Hibernate), кэш будет старым
❌ Требует настройки - нужен Redis/Memcached/Ehcache
❌ Может стать узким местом - если много конкурирующих обращений
Проблема с консистентностью:
// Hibernate: обновили через Hibernate
User user = session.get(User.class, 1L); // Из L2
user.setName("New Name");
session.update(user); // Обновили БД и L2 кэш
// Прямой SQL: обновили напрямую в БД
preparedStatement.executeUpdate("UPDATE users SET name = 'Direct Update' WHERE id = 1");
// Теперь проблема!
User cachedUser = session.get(User.class, 1L); // Вернёт старое значение из кэша!
system.out.println(cachedUser.getName()); // "New Name" - НЕПРАВИЛЬНО!
Решение:
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory;
public void updateUserDirectly(Long userId, String newName) {
// Прямой SQL
String sql = "UPDATE users SET name = ? WHERE id = ?";
// ...
// Очистить кэш
sessionFactory.getCache().evictEntity(User.class, userId);
}
}
3. Query Cache
Query Cache кэширует результаты запросов (HQL, JPQL).
# application.properties
spring.jpa.properties.hibernate.cache.use_query_cache=true
Использование:
@Service
public class UserService {
@Autowired
private EntityManager entityManager;
public List<User> findActiveUsers() {
Query query = entityManager.createQuery(
"SELECT u FROM User u WHERE u.status = :status"
);
query.setParameter("status", "active");
// Включаем Query Cache
query.setHint("org.hibernate.cacheable", true);
return query.getResultList();
}
}
Как работает Query Cache:
Первый запрос:
SELECT u FROM User u WHERE status = 'active'
↓
Выполняем на БД
↓
Кэшируем результаты + список ID
↓
Возвращаем
Второй запрос (одинаковый):
SELECT u FROM User u WHERE status = 'active'
↓
Находим в Query Cache
↓
Берём список ID из кэша
↓
Загружаем объекты из L1/L2 кэша
↓
Возвращаем
⚠️ ВАЖНО: Query Cache зависит от L2 Cache. Если объекты изменяются, Query Cache должен быть инвалидирован.
// Когда создать User, Query Cache должен быть очищен
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory;
public void createUser(User user) {
userRepository.save(user);
// Инвалидировать Query Cache
sessionFactory.getCache().evictQueryRegion("findActiveUsers");
}
}
Преимущества Query Cache:
✅ Кэширует результаты запросов
✅ Хорошо для статических списков
Недостатки:
❌ Сложная инвалидация
❌ Может стать проблемой для изменяемых данных
Таблица сравнения
| Уровень | Область | Управление | Konsistency | Когда использовать |
|---|---|---|---|---|
| L1 (Session) | Одна сессия | Автоматическое | Гарантирована | Всегда |
| L2 (SessionFactory) | Всё приложение | Ручное | Требует синхронизации | Справочники, нередко меняемые объекты |
| Query Cache | Query результаты | Ручное | Требует инвалидации | Статические списки |
Best Practices
1. Используй L1 для связанных объектов
@Service
public class OrderService {
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElse(null);
// Все обращения к customer в этой сессии будут из L1 кэша
Customer customer = order.getCustomer(); // L1
customer.getAddress(); // L1
customer.getOrders(); // L1
}
}
2. Включай L2 только для справочников
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Country { // Справочник
private String code;
private String name;
}
// Но НЕ для часто меняемых данных
// @Cache
public class User { // Часто меняется
private String name;
}
3. Мониторь размер кэша
@Component
public class CacheMonitor {
@Autowired
private SessionFactory sessionFactory;
@Scheduled(fixedRate = 60000)
public void monitorCache() {
Cache cache = sessionFactory.getCache();
// Проверяй размер и очищай если нужно
}
}
4. Будь осторожен с L2 для мутабельных данных
public void updateUser(Long userId, String newName) {
User user = session.get(User.class, userId); // Из L2
user.setName(newName);
session.update(user); // Обновляем L2
// Убедись что L2 синхронизирован с БД
}
Вывод
Три уровня кэширования:
- L1 (Session Cache) - автоматический, всегда активный, в рамках одной сессии
- L2 (SessionFactory Cache) - глобальный, требует настройки, между сессиями
- Query Cache - кэширует результаты запросов, требует инвалидации
Правильное использование кэширования может улучшить performance в 10+ раз, но неправильное использование приведёт к bugs с консистентностью данных.