The second-level cache and when it hurts

Hibernate · lesson 30 of 32 · 6 min read

Cache regions, concurrency strategies, and the query cache people should usually avoid.

Open this lesson in the learning hub

Key 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_ONLY is safe and fast for reference data. READ_WRITE uses soft locks for mutable data. NONSTRICT_READ_WRITE permits brief staleness. TRANSACTIONAL needs 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.