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

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

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

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

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

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

Уровни кэширования в JPA / Hibernate

Кэширование - это критически важный механизм для оптимизации производительности. JPA/Hibernate предоставляет многоуровневую архитектуру кэширования, каждый уровень с собственным масштабом и временем жизни.

1. First-Level Cache (Session/Persistence Context Cache)

Это кэш на уровне транзакции/сессии. Автоматический и всегда включен.

@Service
public class UserService {
  
  @Autowired
  private UserRepository userRepository;
  
  @Transactional
  public void demonstrateFirstLevelCache() {
    // Первый запрос - идет в БД
    User user1 = userRepository.findById(1L).orElse(null);
    System.out.println("User1: " + user1.getName());
    
    // Второй запрос ТОГО ЖЕ объекта - берется из кэша (no SQL query)
    User user2 = userRepository.findById(1L).orElse(null);
    System.out.println("User2: " + user2.getName());
    
    // Это один и тот же объект
    System.out.println(user1 == user2);  // true
    
    // При изменении
    user1.setName("Updated");
    
    // Изменения видны всем ссылкам на этот объект
    System.out.println(user2.getName());  // "Updated"
  }
}

Характеристики:

  • Область видимости: одна транзакция/сессия
  • Тип: Identity-based (по объекту)
  • Управление: Hibernate управляет автоматически
  • Время жизни: от начала транзакции до коммита

Как работает:

Session начинается -> Identity Map пустой
        ↓
findById(1) -> идет в БД -> сохраняется в Identity Map
        ↓
findById(1) -> возвращается из Identity Map (нет SQL)
        ↓
Transaction.commit() -> Identity Map очищается

Пример проблемы - N+1 (если не использовать first-level cache):

// Плохо - БЕЗ first-level cache
@Transactional
public void badExample() {
  User user = userRepository.findById(1L).orElse(null);
  System.out.println(user.getName());  // SELECT 1
  System.out.println(user.getName());  // SELECT 2 (если cache отключен)
}

// Хорошо - WITH first-level cache (по умолчанию)
@Transactional
public void goodExample() {
  User user = userRepository.findById(1L).orElse(null);
  System.out.println(user.getName());  // SELECT 1
  System.out.println(user.getName());  // No query, берется из кэша
}

2. Second-Level Cache (L2 Cache)

Кэш на уровне SessionFactory/ApplicationContext. Общий для всех сессий, но в пределах JVM.

Настройка:

<!-- pom.xml -->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
</dependency>
# application.properties
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=\
  jcache.JCacheRegionFactory
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheManager

# Какие типы операций кэшировать
spring.jpa.properties.hibernate.cache.use_query_cache=true
spring.jpa.properties.hibernate.cache.default_cache_concurrency_strategy=read-write
// Entity уровень
@Entity
@Table(name = "users")
@Cacheable  // Включить кэширование этого entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // Стратегия
public class User {
  @Id
  private Long id;
  private String name;
  private String email;
  
  @OneToMany(mappedBy = "user")
  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
  private List<Order> orders;  // Кэшировать и коллекции
}

Пример использования:

@Service
public class UserService {
  
  @Autowired
  private UserRepository userRepository;
  
  // Сценарий: две разные сессии
  public void demonstrateL2Cache() {
    // Сессия 1
    User user1 = userRepository.findById(1L).orElse(null);
    System.out.println("User1: " + user1.getName());  // SELECT from DB
    
    // Сессия 1 заканчивается (first-level cache очищается)
    
    // Сессия 2 - но L2 cache все еще имеет данные!
    User user2 = userRepository.findById(1L).orElse(null);
    System.out.println("User2: " + user2.getName());  // Берется из L2 cache
    
    // Разные объекты (разные сессии)
    System.out.println(user1 == user2);  // false
    // Но данные одинаковые
    System.out.println(user1.getName().equals(user2.getName()));  // true
  }
}

Стратегии кэширования:

  • READ_ONLY: Данные никогда не изменяются (быстро)
  • READ_WRITE: Нужна синхронизация при изменениях
  • NONSTRICT_READ_WRITE: Расслабленная синхронизация
  • TRANSACTIONAL: Для распределенных транзакций
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)  // Для справочников
public class Country {
  @Id
  private Long id;
  private String name;
}

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // Для изменяемых
public class User {
  @Id
  private Long id;
  private String name;
}

3. Query Cache

Кэширование результатов JPQL/HQL запросов.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
  
  @Query("SELECT u FROM User u WHERE u.status = :status")
  @QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
  List<User> findByStatus(@Param("status") String status);
  
  // Или с Hibernate Session
}

@Service
public class UserService {
  
  @Autowired
  private SessionFactory sessionFactory;
  
  public List<User> getActiveUsers() {
    Session session = sessionFactory.openSession();
    Query<User> query = session.createQuery(
      "FROM User WHERE status = :status",
      User.class
    );
    query.setParameter("status", "ACTIVE");
    query.setCacheable(true);  // Кэшировать результаты этого запроса
    query.setCacheRegion("activeUsers");  // Назвать регион
    
    List<User> users = query.list();
    session.close();
    return users;
  }
}

4. Natural ID Cache

Кэширование по natural ID (альтернативный идентификатор).

@Entity
public class User {
  @Id
  private Long id;
  
  @NaturalId  // email уникален и часто ищется
  private String email;
  
  private String name;
}

@Service
public class UserService {
  
  @Autowired
  private SessionFactory sessionFactory;
  
  public User findByEmail(String email) {
    Session session = sessionFactory.openSession();
    
    // Использует Natural ID cache
    User user = session.bySimpleNaturalId(User.class)
      .load(email);
    
    session.close();
    return user;
  }
}

5. Cache Invalidation и Synchronization

Когда данные меняются, нужно обновлять кэш.

@Service
public class UserService {
  
  @Autowired
  private UserRepository userRepository;
  
  @Autowired
  private SessionFactory sessionFactory;
  
  @Transactional
  public void updateUser(Long userId, String newName) {
    User user = userRepository.findById(userId).orElse(null);
    user.setName(newName);
    userRepository.save(user);  // Это обновит L2 cache автоматически
  }
  
  // Явная инвалидация кэша
  public void invalidateUserCache(Long userId) {
    // Через Statistics API
    Statistics stats = sessionFactory.getStatistics();
    stats.logSummary();
    
    // Или явное удаление
    sessionFactory.getCache().evictEntity(User.class, userId);
    sessionFactory.getCache().evictQueryRegion("User.findById");
  }
  
  // Очистить весь кэш
  public void clearCache() {
    sessionFactory.getCache().evictAllRegions();
  }
}

6. Monitoring и Statistics

@Configuration
public class HibernateStatsConfig {
  
  @Bean
  public HibernateMetrics hibernateMetrics(SessionFactory sessionFactory) {
    sessionFactory.getStatistics().setStatisticsEnabled(true);
    return new HibernateMetrics(sessionFactory);
  }
}

@Service
public class CacheMonitor {
  
  @Autowired
  private SessionFactory sessionFactory;
  
  public void printCacheStats() {
    Statistics stats = sessionFactory.getStatistics();
    
    System.out.println("Entity put count: " + stats.getSecondLevelCachePutCount());
    System.out.println("Entity hit count: " + stats.getSecondLevelCacheHitCount());
    System.out.println("Entity miss count: " + stats.getSecondLevelCacheMissCount());
    
    System.out.println("Query hit count: " + stats.getQueryCacheHitCount());
    System.out.println("Query miss count: " + stats.getQueryCacheMissCount());
    
    // Hit ratio
    long hits = stats.getSecondLevelCacheHitCount();
    long misses = stats.getSecondLevelCacheMissCount();
    double ratio = (double) hits / (hits + misses);
    System.out.println("Hit ratio: " + ratio);
  }
}

7. Проблемы и их решения

Проблема: Stale data (устаревшие данные)

// Если данные изменены напрямую в БД (не через приложение)
// L2 cache не узнает об этом

// Решение: Использовать READ_ONLY для данных что не меняются
// Или явно инвалидировать

Проблема: Изменения в коллекциях не кэшируются

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
  @OneToMany(cascade = CascadeType.ALL)
  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)  // Важно!
  private Set<Order> orders = new HashSet<>();
}

Проблема: Memory leak - кэш растет бесконечно

// Решение: Настроить TTL и размеры в Ehcache
// ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.ehcache.org/v3">
  <cache alias="User">
    <expiry>
      <ttl unit="minutes">30</ttl>  <!-- Expire через 30 минут -->
    </expiry>
    <resources>
      <heap unit="entries">10000</heap>  <!-- Max 10000 объектов -->
    </resources>
  </cache>
</config>

Сравнение уровней кэширования

УровеньОбластьВремя жизниУправлениеСкорость
First-LevelОдна сессияTransactionАвтоматическоеБыстрее всего
Second-LevelSessionFactoryConfigurableАвтоматическоеБыстро
Query CacheSessionFactoryConfigurableАвтоматическоеСредне
Natural IDSessionFactoryConfigurableАвтоматическоеБыстро

Best Practices

  1. Профилируй кэш - убедись что работает
  2. Используй READ_ONLY для статических данных - более эффективно
  3. Кэшируй коллекции если они часто читаются - но оставляй READ_WRITE если меняются
  4. Не кэшируй все подряд - выбирай целевые entities
  5. Настрой TTL и размеры - избегай memory leaks
  6. Тестируй с несколькими сессиями - убедись что синхронизация работает

Кэширование критично для production приложений, но требует внимательности при конфигурации!

Какие знаешь уровни кэширования JPA? | PrepBro