Second-Level Cache with Hibernate in Java

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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper