Overview
Hibernate Second-Level Cache (L2 Cache) is a cache that stores entity data across sessions, reducing database round trips and improving application performance. It works at the SessionFactory level and is shared among all sessions.
Cache Providers
Popular L2 Cache implementations:
- Ehcache (most popular)
- Infinispan
- Hazelcast
- Caffeine
- Redis (distributed caching)
Dependencies
Maven Dependencies
<!-- Hibernate Core --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>6.3.1.Final</version> </dependency> <!-- Ehcache --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jcache</artifactId> <version>6.3.1.Final</version> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.10.8</version> </dependency> <!-- For Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
Configuration
1. Hibernate Configuration
import org.hibernate.cfg.Configuration; import java.util.Properties; public class HibernateConfig { public Configuration getConfiguration() { Configuration configuration = new Configuration(); // Basic database settings configuration.setProperty("hibernate.connection.driver_class", "com.mysql.cj.jdbc.Driver"); configuration.setProperty("hibernate.connection.url", "jdbc:mysql://localhost:3306/testdb"); configuration.setProperty("hibernate.connection.username", "username"); configuration.setProperty("hibernate.connection.password", "password"); configuration.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); // Second-level cache configuration configuration.setProperty("hibernate.cache.use_second_level_cache", "true"); configuration.setProperty("hibernate.cache.use_query_cache", "true"); configuration.setProperty("hibernate.cache.region.factory_class", "jcache"); configuration.setProperty("hibernate.javax.cache.provider", "org.ehcache.jsr107.EhcacheCachingProvider"); configuration.setProperty("hibernate.javax.cache.uri", "/ehcache.xml"); // Additional cache settings configuration.setProperty("hibernate.cache.use_structured_entries", "true"); configuration.setProperty("hibernate.cache.auto_evict_collection_cache", "true"); return configuration; } } 2. Spring Boot Configuration
# application.yml spring: jpa: properties: hibernate: # Second-level cache configuration cache: use_second_level_cache: true use_query_cache: true region: factory_class: jcache javax: cache: provider: org.ehcache.jsr107.EhcacheCachingProvider # Show cache operations in logs generate_statistics: true # Auto export statistics via JMX jmx: enabled: true # Show SQL and cache operations show-sql: true hibernate: ddl-auto: update # Cache configuration logging: level: org.hibernate.cache: DEBUG
3. Ehcache Configuration (ehcache.xml)
<?xml version="1.0" encoding="UTF-8"?> <config xmlns='http://www.ehcache.org/v3' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'> <!-- Default template for all caches --> <cache-template name="default"> <expiry> <ttl unit="minutes">30</ttl> </expiry> <heap unit="entries">1000</heap> </cache-template> <!-- Specific cache regions --> <cache alias="com.example.entities.Product"> <expiry> <ttl unit="minutes">60</ttl> </expiry> <heap unit="entries">5000</heap> <jsr107:mbeans enable-statistics="true"/> </cache> <cache alias="com.example.entities.Category"> <expiry> <ttl unit="hours">2</ttl> </expiry> <heap unit="entries">1000</heap> </cache> <cache alias="com.example.entities.User"> <expiry> <ttl unit="minutes">15</ttl> </expiry> <heap unit="entries">2000</heap> </cache> <!-- Query cache --> <cache alias="default-query-results-region"> <expiry> <ttl unit="minutes">10</ttl> </expiry> <heap unit="entries">1000</heap> </cache> <!-- Collection cache --> <cache alias="com.example.entities.Product.categories"> <expiry> <ttl unit="minutes">30</ttl> </expiry> <heap unit="entries">1000</heap> </cache> </config>
Entity Configuration
1. Cacheable Entities
import javax.persistence.*; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Entity @Table(name = "products") @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "product") @NamedQueries({ @NamedQuery( name = "Product.findByCategory", query = "SELECT p FROM Product p WHERE p.category = :category", hints = { @QueryHint(name = "org.hibernate.cacheable", value = "true"), @QueryHint(name = "org.hibernate.cacheRegion", value = "product-queries") } ) }) public class Product implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", nullable = false) private String name; @Column(name = "description") private String description; @Column(name = "price") private Double price; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) private Category category; @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) private List<ProductReview> reviews = new ArrayList<>(); @Version @Column(name = "version") private Long version; // Constructors public Product() {} public Product(String name, String description, Double price) { this.name = name; this.description = description; this.price = price; } // Getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public Category getCategory() { return category; } public void setCategory(Category category) { this.category = category; } public List<ProductReview> getReviews() { return reviews; } public void setReviews(List<ProductReview> reviews) { this.reviews = reviews; } public Long getVersion() { return version; } public void setVersion(Long version) { this.version = version; } // Helper methods public void addReview(ProductReview review) { reviews.add(review); review.setProduct(this); } public void removeReview(ProductReview review) { reviews.remove(review); review.setProduct(null); } } 2. Category Entity with Collection Cache
@Entity @Table(name = "categories") @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "category") public class Category implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", unique = true, nullable = false) private String name; @Column(name = "description") private String description; @OneToMany(mappedBy = "category", fetch = FetchType.LAZY) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) private List<Product> products = new ArrayList<>(); @Version private Long version; // Constructors, getters, and setters public Category() {} public Category(String name, String description) { this.name = name; this.description = description; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public List<Product> getProducts() { return products; } public void setProducts(List<Product> products) { this.products = products; } public Long getVersion() { return version; } public void setVersion(Long version) { this.version = version; } } 3. Review Entity
@Entity @Table(name = "product_reviews") @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "review") public class ProductReview implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "review_text", length = 1000) private String reviewText; @Column(name = "rating") private Integer rating; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id") private Product product; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) private User user; @Version private Long version; // Constructors, getters, and setters public ProductReview() {} public ProductReview(String reviewText, Integer rating) { this.reviewText = reviewText; this.rating = rating; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getReviewText() { return reviewText; } public void setReviewText(String reviewText) { this.reviewText = reviewText; } public Integer getRating() { return rating; } public void setRating(Integer rating) { this.rating = rating; } public Product getProduct() { return product; } public void setProduct(Product product) { this.product = product; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Long getVersion() { return version; } public void setVersion(Long version) { this.version = version; } } Cache Concurrency Strategies
1. READ_ONLY Strategy
@Entity @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) public class Country implements Serializable { @Id private String code; private String name; // Immutable entity - no setters or private setters public Country(String code, String name) { this.code = code; this.name = name; } // Getters only public String getCode() { return code; } public String getName() { return name; } } 2. READ_WRITE Strategy
@Entity @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class UserSession implements Serializable { @Id private String sessionId; private Long userId; private Date lastAccessTime; private Map<String, Object> attributes; // Uses soft locks for concurrent access } 3. NONSTRICT_READ_WRITE Strategy
@Entity @Cacheable @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class Configuration implements Serializable { @Id private String key; private String value; private Date lastModified; // Minimal locking, eventual consistency } 4. TRANSACTIONAL Strategy
@Entity @Cacheable @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) public class BankAccount implements Serializable { @Id private Long accountNumber; private BigDecimal balance; // Full JTA transaction support } Repository Implementation with Cache
1. Product Repository with Cache Support
import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import javax.persistence.*; import java.util.List; import java.util.Optional; @Repository @Transactional public class ProductRepository { @PersistenceContext private EntityManager entityManager; // Find by ID - will use L2 cache if configured public Optional<Product> findById(Long id) { return Optional.ofNullable(entityManager.find(Product.class, id)); } // Save product - will update cache public Product save(Product product) { if (product.getId() == null) { entityManager.persist(product); return product; } else { return entityManager.merge(product); } } // Cacheable query public List<Product> findByCategory(Category category) { return entityManager .createNamedQuery("Product.findByCategory", Product.class) .setParameter("category", category) .setHint("org.hibernate.cacheable", true) .getResultList(); } // Custom cacheable query public List<Product> findExpensiveProducts(Double minPrice) { return entityManager .createQuery("SELECT p FROM Product p WHERE p.price > :minPrice", Product.class) .setParameter("minPrice", minPrice) .setHint("org.hibernate.cacheable", true) .setHint("org.hibernate.cacheRegion", "expensive-products") .getResultList(); } // Bulk update - will evict cache public int updateProductPrices(Double percentageIncrease) { int updatedCount = entityManager .createQuery("UPDATE Product p SET p.price = p.price * (1 + :increase)") .setParameter("increase", percentageIncrease / 100) .executeUpdate(); // Evict all products from cache after bulk update evictProductCache(); return updatedCount; } // Evict cache programmatically public void evictProductCache() { EntityManagerFactory entityManagerFactory = entityManager.getEntityManagerFactory(); Cache cache = entityManagerFactory.getCache(); // Evict specific entity cache.evict(Product.class); // Evict all entities // cache.evictAll(); } // Check if entity is in cache public boolean isProductInCache(Long productId) { Cache cache = entityManager.getEntityManagerFactory().getCache(); return cache.contains(Product.class, productId); } } Service Layer with Cache Management
1. Product Service
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Service @Transactional public class ProductService { @Autowired private ProductRepository productRepository; @Autowired private CategoryRepository categoryRepository; // This method uses Spring Cache alongside Hibernate L2 cache @Cacheable(value = "products", key = "#id") public Optional<Product> getProductById(Long id) { return productRepository.findById(id); } @CachePut(value = "products", key = "#product.id") public Product createProduct(Product product) { return productRepository.save(product); } @CachePut(value = "products", key = "#product.id") public Product updateProduct(Product product) { return productRepository.save(product); } @CacheEvict(value = "products", key = "#id") public void deleteProduct(Long id) { productRepository.findById(id).ifPresent(product -> { // Additional cleanup if needed productRepository.delete(product); }); } @CacheEvict(value = "products", allEntries = true) public void evictProductCache() { // Manual cache eviction } public List<Product> getProductsByCategory(String categoryName) { Optional<Category> category = categoryRepository.findByName(categoryName); return category.map(productRepository::findByCategory) .orElse(List.of()); } public List<Product> searchProducts(String keyword, Double maxPrice) { // Implementation with potential cache hints return List.of(); // Simplified } // Bulk operation with cache management public void applyDiscountToCategory(String categoryName, Double discountPercent) { List<Product> products = getProductsByCategory(categoryName); for (Product product : products) { Double newPrice = product.getPrice() * (1 - discountPercent / 100); product.setPrice(newPrice); productRepository.save(product); } // Cache will be updated automatically due to @CachePut on save methods } } Cache Statistics and Monitoring
1. Statistics Bean
import org.hibernate.SessionFactory; import org.hibernate.stat.Statistics; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.persistence.EntityManagerFactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class CacheStatistics { @Autowired private EntityManagerFactory entityManagerFactory; private final Map<String, CacheStats> cacheStats = new ConcurrentHashMap<>(); public Statistics getHibernateStatistics() { SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); return sessionFactory.getStatistics(); } public CacheStats getCacheStatistics(String regionName) { Statistics stats = getHibernateStatistics(); long hitCount = stats.getSecondLevelCacheHitCount(); long missCount = stats.getSecondLevelCacheMissCount(); long putCount = stats.getSecondLevelCachePutCount(); return new CacheStats(regionName, hitCount, missCount, putCount); } public void printCacheStatistics() { Statistics stats = getHibernateStatistics(); System.out.println("=== Hibernate L2 Cache Statistics ==="); System.out.println("Second Level Cache Hit Count: " + stats.getSecondLevelCacheHitCount()); System.out.println("Second Level Cache Miss Count: " + stats.getSecondLevelCacheMissCount()); System.out.println("Second Level Cache Put Count: " + stats.getSecondLevelCachePutCount()); String[] regionNames = stats.getSecondLevelCacheRegionNames(); for (String regionName : regionNames) { System.out.println("Region: " + regionName); System.out.println(" - Hit Count: " + stats.getSecondLevelCacheHitCount(regionName)); System.out.println(" - Miss Count: " + stats.getSecondLevelCacheMissCount(regionName)); System.out.println(" - Put Count: " + stats.getSecondLevelCachePutCount(regionName)); } } public static class CacheStats { private final String regionName; private final long hitCount; private final long missCount; private final long putCount; public CacheStats(String regionName, long hitCount, long missCount, long putCount) { this.regionName = regionName; this.hitCount = hitCount; this.missCount = missCount; this.putCount = putCount; } public double getHitRatio() { long total = hitCount + missCount; return total > 0 ? (double) hitCount / total : 0.0; } // Getters public String getRegionName() { return regionName; } public long getHitCount() { return hitCount; } public long getMissCount() { return missCount; } public long getPutCount() { return putCount; } } } 2. Cache Monitoring Controller
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.stream.Collectors; @RestController @RequestMapping("/api/cache") public class CacheMonitorController { @Autowired private CacheStatistics cacheStatistics; @Autowired private ProductService productService; @GetMapping("/statistics") public Map<String, Object> getCacheStatistics() { Statistics stats = cacheStatistics.getHibernateStatistics(); return Map.of( "secondLevelCacheHitCount", stats.getSecondLevelCacheHitCount(), "secondLevelCacheMissCount", stats.getSecondLevelCacheMissCount(), "secondLevelCachePutCount", stats.getSecondLevelCachePutCount(), "queryCacheHitCount", stats.getQueryCacheHitCount(), "queryCacheMissCount", stats.getQueryCacheMissCount(), "queryCachePutCount", stats.getQueryCachePutCount(), "regions", stats.getSecondLevelCacheRegionNames() ); } @PostMapping("/evict/{region}") public String evictCacheRegion(@PathVariable String region) { EntityManagerFactory emf = // get entity manager factory emf.getCache().evict(Class.forName(region)); return "Cache region evicted: " + region; } @PostMapping("/evict-all") public String evictAllCache() { EntityManagerFactory emf = // get entity manager factory emf.getCache().evictAll(); productService.evictProductCache(); return "All caches evicted"; } } Advanced Cache Configuration
1. Custom Cache Region Factory
import org.hibernate.cache.spi.RegionFactory; import org.hibernate.cache.spi.support.RegionNameQualifier; import org.springframework.cache.CacheManager; public class CustomCacheRegionFactory implements RegionFactory { private final CacheManager cacheManager; public CustomCacheRegionFactory(CacheManager cacheManager) { this.cacheManager = cacheManager; } @Override public void start(Settings settings, Properties properties) throws CacheException { // Initialization logic } @Override public void stop() { // Cleanup logic } @Override public boolean isMinimalPutsEnabledByDefault() { return true; } @Override public Access buildEntityRegion(String regionName, SessionFactoryOptions settings, CacheDataDescription metadata) throws CacheException { String qualifiedRegionName = RegionNameQualifier.qualify(regionName, settings); return new EntityCacheAdapter(qualifiedRegionName, cacheManager); } // Implement other region building methods... } 2. Distributed Cache with Redis
# application-redis.yml spring: cache: type: redis redis: time-to-live: 3600000 # 1 hour cache-null-values: false redis: host: localhost port: 6379 password: timeout: 2000 # Hibernate configuration for Redis hibernate: cache: use_second_level_cache: true use_query_cache: true region: factory_class: com.example.RedisRegionFactory
Testing Cache Behavior
1. Cache Integration Tests
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceUnit; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class SecondLevelCacheTest { @Autowired private ProductService productService; @Autowired private ProductRepository productRepository; @PersistenceUnit private EntityManagerFactory entityManagerFactory; @Test void testEntityCache() { // First call - should hit database Product product1 = productService.getProductById(1L).orElseThrow(); // Clear first level cache EntityManager em = entityManagerFactory.createEntityManager(); em.clear(); // Second call - should hit L2 cache Product product2 = productService.getProductById(1L).orElseThrow(); // Verify it's the same instance (from cache) assertSame(product1.getId(), product2.getId()); } @Test void testQueryCache() { // First query execution List<Product> products1 = productService.getProductsByCategory("Electronics"); // Second query execution - should hit query cache List<Product> products2 = productService.getProductsByCategory("Electronics"); assertEquals(products1.size(), products2.size()); } @Test void testCacheEviction() { Product product = productService.getProductById(1L).orElseThrow(); // Update product product.setPrice(999.99); productService.updateProduct(product); // Verify cache was updated Product cachedProduct = productService.getProductById(1L).orElseThrow(); assertEquals(999.99, cachedProduct.getPrice()); } @Test void testCacheStatistics() { Statistics stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics(); stats.clear(); // Perform operations productService.getProductById(1L); productService.getProductById(1L); // This should hit cache assertEquals(1, stats.getSecondLevelCacheHitCount()); assertTrue(stats.getSecondLevelCacheMissCount() >= 0); } } Best Practices and Performance Tips
1. Cache Configuration Best Practices
@Component public class CacheBestPractices { // 1. Choose appropriate concurrency strategy public void demonstrateStrategySelection() { // READ_ONLY: Immutable data (countries, categories) // READ_WRITE: Frequently read, occasionally updated // NONSTRICT_READ_WRITE: Rarely updated, consistency not critical // TRANSACTIONAL: Critical data with high consistency requirements } // 2. Monitor cache hit ratios public void monitorCachePerformance() { // Aim for > 80% hit ratio for frequently accessed data // Adjust TTL and cache size based on monitoring } // 3. Use appropriate cache regions public void configureCacheRegions() { // Separate cache regions for different entity types // Different TTL and size limits based on access patterns } // 4. Handle cache synchronization public void handleCacheSyncInCluster() { // Use distributed cache in clustered environments // Implement cache invalidation protocols } } 2. Common Pitfalls and Solutions
@Component public class CachePitfalls { // 1. Cache bloat - too many entities cached public void preventCacheBloat() { // Use appropriate cache size limits // Implement cache eviction policies // Cache only frequently accessed data } // 2. Stale data issues public void handleStaleData() { // Use appropriate TTL settings // Implement cache invalidation on updates // Use versioning for optimistic locking } // 3. Memory issues public void manageMemory() { // Monitor cache memory usage // Use off-heap storage for large caches // Implement proper serialization for distributed caches } // 4. Transaction boundaries public void handleTransactions() { // Be aware of cache updates within transactions // Use appropriate isolation levels // Handle rollback scenarios } } This comprehensive guide covers Hibernate Second-Level Cache implementation, configuration, monitoring, and best practices for optimal performance in Java applications.