← Назад к вопросам

Какие уровни кэширования знаешь в Hibernate

1.7 Middle🔥 121 комментариев
#ORM и Hibernate#Кэширование и NoSQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Уровни кэширования в 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 CacheQuery результатыРучноеТребует инвалидацииСтатические списки

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 синхронизирован с БД
}

Вывод

Три уровня кэширования:

  1. L1 (Session Cache) - автоматический, всегда активный, в рамках одной сессии
  2. L2 (SessionFactory Cache) - глобальный, требует настройки, между сессиями
  3. Query Cache - кэширует результаты запросов, требует инвалидации

Правильное использование кэширования может улучшить performance в 10+ раз, но неправильное использование приведёт к bugs с консистентностью данных.