introduction
In the development of enterprise-level Java applications, database query performance often becomes a key bottleneck that restricts the overall performance of the system. Especially when dealing with complex business logic, applications often need to perform a large number of repeated query operations. Although these queries are the same parameters, they trigger database access each time, causing unnecessary performance losses.
The Hibernate query caching mechanism provides an effective technical solution to solve this problem by caching query result sets. Query cache can temporarily store the results of HQL, Criteria API or native SQL queries in memory, and directly return the cached results when the same query is executed again, thereby significantly reducing the number of database interactions and improving application response speed.
1. Query cache core mechanism
How cache works
The query cache generates a unique cache key value based on the combination of query statements and parameters, and stores the query result set in the cache area as an identifier list. Unlike entity caches, query cache stores not complete entity objects, but a collection of primary keys of entities. When the cache hits, Hibernate gets the complete entity data from the secondary cache based on these primary keys. This design ensures coordination between query cache and entity cache while avoiding data redundancy. The entry into effect of query cache requires the activation of level 2 cache support at the same time, and the two complement each other to build a complete cache system.
/** * Query cache configuration management class */ @Configuration @EnableTransactionManagement public class HibernateCacheConfig { /** * Configure SessionFactory and enable query cache */ @Bean public LocalSessionFactoryBean sessionFactory() { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); (dataSource()); (""); Properties hibernateProperties = new Properties(); // Enable Level 2 cache (".use_second_level_cache", "true"); // Enable query cache (".use_query_cache", "true"); // Configure the cache provider (".factory_class", ""); // Turn on statistics information collection ("hibernate.generate_statistics", "true"); (hibernateProperties); return sessionFactory; } /** * Configure data source */ @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); ("jdbc:mysql://localhost:3306/testdb"); ("root"); ("password"); (""); return dataSource; } }
Cache key-value generation strategy
The key value generation of the query cache is based on the hash value, parameter value, and query configuration of the query statement. Hibernate will comprehensively consider HQL statement text, type and value of binding parameters, paging settings, sorting conditions and other information to build a unique cache identifier.
This precise key-value generation mechanism ensures cache hit accuracy under the same query conditions, while avoiding cache conflicts between different queries. The processing of parameter binding is particularly important. Even if the query statement is the same, different parameters will generate different cache key values, ensuring the correctness of the query results.
/** * Product entity class, support query cache */ @Entity @Table(name = "product") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Product { @Id @GeneratedValue(strategy = ) private Long id; @Column(name = "name") private String name; @Column(name = "category_id") private Long categoryId; @Column(name = "price") private BigDecimal price; @Column(name = "status") @Enumerated() private ProductStatus status; @Column(name = "create_time") @Temporal() private Date createTime; // Constructor public Product() {} public Product(String name, Long categoryId, BigDecimal price) { = name; = categoryId; = price; = ; = new Date(); } // Getter and setter methods are omitted /** * Product status enumeration */ public enum ProductStatus { ACTIVE, INACTIVE, DISCONTINUED } }
2. HQL query cache implementation
Basic query cache configuration
Enable HQL query cache requires explicit call to the setCacheable method on the Query object and optionally specify the cache area name. The configuration of the cache area allows developers to set differentiated cache strategies for different types of queries, including cache size, survival time and elimination algorithms. Reasonable zoning can help improve the accuracy and efficiency of cache management. The effectiveness of query cache also depends on the stability of query conditions. Frequently changing query parameters may lead to a decrease in cache hit rate, which needs to be traded down based on actual business scenarios.
/** * Product query service category */ @Service @Transactional(readOnly = true) public class ProductQueryService { @Autowired private SessionFactory sessionFactory; /** * Query products according to classification, enable query cache */ public List<Product> findProductsByCategory(Long categoryId) { Session session = (); Query<Product> query = ( "FROM Product p WHERE = :categoryId AND = :status ORDER BY DESC", ); ("categoryId", categoryId); ("status", ); // Enable query cache (true); // Specify the cache area ("productsByCategory"); return (); } /** * Price range query, use query cache */ public List<Product> findProductsByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) { Session session = (); Query<Product> query = ( "FROM Product p WHERE BETWEEN :minPrice AND :maxPrice " + "AND = :status ORDER BY ASC", ); ("minPrice", minPrice); ("maxPrice", maxPrice); ("status", ); // Enable query cache (true); ("productsByPriceRange"); return (); } /** * Get popular products, suitable for querying cache */ public List<Product> getPopularProducts(int limit) { Session session = (); Query<Product> query = ( "FROM Product p WHERE = :status ORDER BY DESC", ); ("status", ); (limit); // Enable query cache, popular products are querying frequently (true); ("popularProducts"); return (); } }
Complex query caching strategy
The caching strategy in complex query scenarios needs to consider the business characteristics of the query and the frequency of data updates. Query caches can also work for complex HQL statements that contain associative queries, aggregate functions, or subqueries, but require more careful evaluation of cache benefits. The caching of associated queries needs to ensure that all relevant entities support L2 cache, otherwise there may be a mixture of some data from the cache and some data from the database. The cache of aggregated queries is particularly suitable for report-type queries, which are usually complex in calculations but relatively stable in results.
/** * Complex query caching service */ @Service @Transactional(readOnly = true) public class AdvancedQueryService { @Autowired private SessionFactory sessionFactory; /** * Classified statistics query, using aggregate cache */ public List<CategoryStatistics> getCategoryStatistics() { Session session = (); Query<CategoryStatistics> query = ( "SELECT new (" + ", COUNT(p), AVG(), MAX(), MIN()) " + "FROM Product p WHERE = :status " + "GROUP BY ORDER BY COUNT(p) DESC", ); ("status", ); // Statistical queries are suitable for caching, with complex calculations but infrequent changes (true); ("categoryStatistics"); return (); } /** * Associative query cache example */ public List<Product> findProductsWithCategoryInfo(String categoryName) { Session session = (); Query<Product> query = ( "SELECT p FROM Product p JOIN Category c ON = " + "WHERE = :categoryName AND = :status " + "ORDER BY DESC", ); ("categoryName", categoryName); ("status", ); // Associate query cache to ensure that Category entities also support Level 2 cache (true); ("productsWithCategory"); return (); } /** * Pagination query cache processing */ public Page<Product> findProductsPaged(int pageNumber, int pageSize, Long categoryId) { Session session = (); // Total number of queries Query<Long> countQuery = ( "SELECT COUNT(p) FROM Product p WHERE = :categoryId AND = :status", ); ("categoryId", categoryId); ("status", ); (true); ("productCount"); Long totalCount = (); // Pagination query data Query<Product> dataQuery = ( "FROM Product p WHERE = :categoryId AND = :status ORDER BY DESC", ); ("categoryId", categoryId); ("status", ); (pageNumber * pageSize); (pageSize); // Pagination queries can also be cached (true); ("productsPaged"); List<Product> products = (); return new Page<>(products, pageNumber, pageSize, totalCount); } }
3. Criteria API query cache
Criteria query cache configuration
The Criteria API provides an object-oriented query construction method and also supports query caching function. Query conditions built through CriteriaQuery and CriteriaBuilder can enable the caching mechanism before TypedQuery is executed. The cached key value generation of Criteria query is based on the combination of query conditions, including selection conditions, sorting rules, connection relationships and other elements. This method is particularly suitable for dynamic query scenarios, and corresponding cache policies can be generated based on different business conditions.
/** * Criteria query cache service */ @Service @Transactional(readOnly = true) public class CriteriaQueryService { @Autowired private SessionFactory sessionFactory; /** * Build cache queries with Criteria */ public List<Product> findProductsByCriteria(ProductSearchCriteria criteria) { Session session = (); CriteriaBuilder cb = (); CriteriaQuery<Product> cq = (); Root<Product> root = (); // Build query conditions Predicate predicate = (); if (() != null) { predicate = (predicate, (("categoryId"), ())); } if (() != null) { predicate = (predicate, (("price"), ())); } if (() != null) { predicate = (predicate, (("price"), ())); } predicate = (predicate, (("status"), )); (predicate); ((("createTime"))); TypedQuery<Product> query = (cq); // Enable Criteria query cache (true); ("criteriaProductSearch"); return (); } /** * Dynamic statistics query cache */ public ProductSummary getProductSummaryByCriteria(Long categoryId) { Session session = (); CriteriaBuilder cb = (); CriteriaQuery<Object[]> cq = (Object[].class); Root<Product> root = (); // Build multi-field aggregation query ( (root), (("price")), (("price")), (("price")) ); Predicate predicate = ( (("categoryId"), categoryId), (("status"), ) ); (predicate); TypedQuery<Object[]> query = (cq); // Statistical query cache (true); ("productSummary"); Object[] result = (); return new ProductSummary( ((Long) result[0]).intValue(), (Double) result[1], (BigDecimal) result[2], (BigDecimal) result[3] ); } }
Dynamic query cache optimization
Cache optimization in dynamic query scenarios requires balancing query flexibility and cache efficiency. Overly refined query conditions combinations may lead to cache fragmentation and reduce overall hit rate. A reasonable strategy is to identify high-frequency query patterns in the business and specifically design cache areas and policies for these patterns. Through standardized processing of query conditions, similar queries can be merged into the same cache key value to improve the cache reuse rate.
/** * Query the conditional encapsulation class */ public class ProductSearchCriteria { private Long categoryId; private BigDecimal minPrice; private BigDecimal maxPrice; private String nameKeyword; private status; // Constructor public ProductSearchCriteria() { = ; } /** * Helpful method for generating cached key values */ public String generateCacheKey() { StringBuilder keyBuilder = new StringBuilder(); ("category:").append(categoryId != null ? categoryId : "all"); ("_price:").append(minPrice != null ? minPrice : "0"); ("-").append(maxPrice != null ? maxPrice : "unlimited"); ("_keyword:").append(nameKeyword != null ? nameKeyword : "none"); ("_status:").append(status); return (); } // Getter and setter methods are omitted} /** * Product summary information category */ public class ProductSummary { private int totalCount; private double averagePrice; private BigDecimal maxPrice; private BigDecimal minPrice; public ProductSummary(int totalCount, double averagePrice, BigDecimal maxPrice, BigDecimal minPrice) { = totalCount; = averagePrice; = maxPrice; = minPrice; } // Getter and setter methods are omitted}
4. Cache failure and consistency management
Automatic failure mechanism
The automatic failure mechanism of query cache ensures data consistency. When the relevant entity adds, deletes and modifys operations, Hibernate will automatically clean up the relevant query cache entries. The failed granularity control is based on the cache area and entity type. The system will identify which query caches may be affected by data changes and perform corresponding cleaning operations. Although this mechanism ensures data consistency, it may lead to excessive cache failure, especially in high concurrent write scenarios.
/** * Product management services, display cache failure processing */ @Service @Transactional public class ProductManagementService { @Autowired private SessionFactory sessionFactory; /** * Create a product and trigger query cache invalidation */ public Product createProduct(Product product) { Session session = (); // Save operation will automatically trigger the invalidation of the related query cache (product); // You can manually clean up specific query cache areas ().evictQueryRegion("productsByCategory"); ().evictQueryRegion("categoryStatistics"); return product; } /** * Update product information */ public Product updateProduct(Product product) { Session session = (); // Get the product information before the update to determine which caches need to be cleaned Product existingProduct = (, ()); // Perform update operation (product); // Decide the cache cleaning strategy based on the updated content if (!().equals(())) { //Classification changes, clean up classification-related caches ().evictQueryRegion("productsByCategory"); ().evictQueryRegion("categoryStatistics"); } if (!().equals(())) { // Price changes, clear price-related cache ().evictQueryRegion("productsByPriceRange"); } return product; } /** * Cache processing for batch update operations */ public void batchUpdateProductStatus(List<Long> productIds, newStatus) { Session session = (); // Perform batch updates Query updateQuery = ( "UPDATE Product p SET = :newStatus WHERE IN :ids"); ("newStatus", newStatus); ("ids", productIds); int updatedCount = (); // Clean up the relevant query cache after batch operation if (updatedCount > 0) { ().evictQueryRegions(); } } }
Cache monitoring and optimization
Query cache monitoring requires attention to key indicators such as hit rate, failure frequency and memory usage. The Hibernate Statistics API can obtain detailed cached running data, providing a basis for performance tuning. Analysis of monitoring data helps identify cache configuration issues, including optimization space in terms of cache area size settings, survival time configuration, and failure strategies.
/** * Query cache monitoring service */ @Service public class QueryCacheMonitorService { @Autowired private SessionFactory sessionFactory; /** * Generate query cache performance report */ public QueryCacheReport generateCacheReport() { Statistics stats = (); QueryCacheReport report = new QueryCacheReport(); // Query cache hit statistics (()); (()); (()); // Calculate hit rate long totalQueryRequests = () + (); if (totalQueryRequests > 0) { double hitRatio = (double) () / totalQueryRequests; (hitRatio); } // Level 2 cache statistics (()); (()); // Query execution statistics (()); (()); return report; } /** * Clean the specified query cache area */ public void evictQueryCache(String regionName) { if (regionName != null && !()) { ().evictQueryRegion(regionName); } else { ().evictDefaultQueryRegion(); } } /** * Get cache area information */ public Map<String, CacheRegionInfo> getCacheRegionInfo() { Statistics stats = (); Map<String, CacheRegionInfo> regionInfoMap = new HashMap<>(); String[] regionNames = (); for (String regionName : regionNames) { SecondLevelCacheStatistics regionStats = (regionName); CacheRegionInfo info = new CacheRegionInfo(); (regionName); (()); (()); (()); (()); (()); (regionName, info); } return regionInfoMap; } } /** * Query cache report class */ public class QueryCacheReport { private long queryCacheHitCount; private long queryCacheMissCount; private long queryCachePutCount; private double hitRatio; private long secondLevelCacheHitCount; private long secondLevelCacheMissCount; private long queryExecutionCount; private long queryExecutionMaxTime; // Getter and setter methods are omitted @Override public String toString() { return ( "Query Cache Report:\n" + "Hit: %d, Miss: %d, Put: %d\n" + "Hit Ratio: %.2f%%\n" + "Query Execution Count: %d, Max Time: %d ms\n" + "Second Level Cache Hit: %d, Miss: %d", queryCacheHitCount, queryCacheMissCount, queryCachePutCount, hitRatio * 100, queryExecutionCount, queryExecutionMaxTime, secondLevelCacheHitCount, secondLevelCacheMissCount ); } }
Summarize
Hibernate query cache is an important technical means to improve the data access performance of Java applications. It effectively reduces the access pressure of repeated queries to the database by cached query result sets. The collaborative working mechanism between query cache and level 2 cache ensures the integrity and efficiency of the cache system, providing reliable performance guarantees for enterprise-level applications.
In actual applications, developers need to reasonably configure cache policies based on business characteristics, including key elements such as cache area division, survival time setting and failure mechanism configuration. Through continuous monitoring and tuning, the performance benefits of query cache can be maximized while ensuring data consistency. Mastering the core principles and practical skills of query caching will help build a more efficient and stable Java data access layer, laying a solid foundation for the performance improvement of the entire application system.
The above is personal experience. I hope you can give you a reference and I hope you can support me more.