The second-level cache and when it hurts
Cache regions, concurrency strategies, and the query cache people should usually avoid.
Open this lesson in the learning hubKey points
- The first-level cache is the persistence context and dies with the transaction. The second-level cache lives in the SessionFactory and is shared across sessions - which is where all the difficulty is.
- The concurrency strategy is the important choice.
READ_ONLYis safe and fast for reference data.READ_WRITEuses soft locks for mutable data.NONSTRICT_READ_WRITEpermits brief staleness.TRANSACTIONALneeds a JTA provider. - It only caches by id. A query returning entities still executes its SQL; the cache only avoids loading each entity afterwards - which is why it helps far less than people expect on query-heavy code.
- The query cache is the trap. It caches result ids per query and parameter set, and any write to a table invalidates every cached query touching it. On a write-heavy table it is pure overhead.
- In a clustered deployment a local cache diverges between instances. Either accept the staleness deliberately, or run a distributed cache and accept the network cost - there is no free option.
- Measure before enabling. Hibernate statistics report hit and miss counts per region, and a region with a poor hit ratio is costing memory and invalidation work for nothing.
Example
// Reference data that essentially never changes - the ideal case.
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region = "country")
class Country { @Id private String iso; private String name; }
// Mutable data - soft locks around writes.
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "product")
class Product {
@Id private Long id;
private BigDecimal price;
// Collections are cached SEPARATELY and need their own annotation.
@OneToMany(mappedBy = "product")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Review> reviews;
}
---
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: false # off by default, and usually correct
region.factory_class: jcache
javax.cache.provider: org.ehcache.jsr107.EhcacheCachingProvider
---
/*
* WHY THE QUERY CACHE USUALLY LOSES:
*
* select p from Product p where p.category = :c
*
* cached: [id 1, id 7, id 12] keyed by query + parameters
*
* ANY insert, update or delete on product invalidates EVERY cached
* query over that table - not just the affected rows.
*
* On a table written once a second, the cache is invalidated once a
* second and you have paid for bookkeeping that never pays back.
*
* Check the hit ratio before believing any of this helps:
* stats.getSecondLevelCacheHitCount()
* stats.getSecondLevelCacheMissCount()
* a region below ~80% hits is usually not worth keeping
*/
The second-level cache works by id, and the query cache is invalidated by any write to the table - measure the hit ratio before trusting either.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Hibernate course, and every lesson in it is listed on the Hibernate contents page.