Caching in Hibernate is a multi-level system, and understanding its architecture is critical for application performance. The golden rule: caching is disabled by default (except for the first-level cache), and it must be enabled consciously for specific data. The system consists of three independent layers, each with its own area of responsibility.
First-Level Cache
This is the foundation that cannot be disabled. It is always active and bound to a specific session (Session/EntityManager).
- What it is: Essentially, this is the Persistence Context. All entities loaded within a single session are placed into this cache.
- Scope: Only within a single session (usually a single transaction). Once the session is closed, the cache is destroyed.
- Guarantees: It ensures the identity map (a repeated
getfor the same ID returns the exact same Java object instance) and dirty checking (Hibernate compares the current state with the snapshot uponflush). - Management: You cannot disable it, but you can manage its contents:
session.evict(entity)removes a specific object, andsession.clear()completely empties the cache. This is highly useful during batch processing of large data volumes to prevent Out-Of-Memory (OOM) errors.
Second-Level Cache
This is a shared cache that lives at the SessionFactory level and is shared across all sessions. It is designed for data that is rarely updated and frequently read.
- Scope: Process level (or cluster level, if a distributed provider is used). Data survives session closure.
- What is cached: Entity state (by their ID). It does not cache query results.
- Disabled by default. To enable it, three conditions must be met:
- Activate the cache in the settings (
hibernate.cache.use_second_level_cache=true). - Provide a cache provider (Ehcache, Caffeine, Infinispan, Redis, etc.).
- Explicitly mark each entity with the
@Cacheannotation. Global activation does not cache everything indiscriminately — entities must "opt-in" to caching.
- Activate the cache in the settings (
Concurrency Strategies (CacheConcurrencyStrategy):
When configuring @Cache, you must specify a strategy that defines how the cache behaves during concurrent updates.
| Strategy | When to use | Characteristics |
|---|---|---|
READ_ONLY |
Immutable reference data | Fastest and safest. Throws an exception on update attempts. |
NONSTRICT_READ_WRITE |
Updates are rare and unlikely | No locks. Slight risk of reading stale data during an update. |
READ_WRITE |
Updates are possible | Uses "soft" locks to prevent reading stale data during a transaction. |
TRANSACTIONAL |
Strict consistency required | Full transactional locks. Requires explicit support from the cache provider. |
Management: sessionFactory.evict(Entity.class, id) allows targeted removal of an entity from the second-level cache.
Query Cache
- This is a specialized cache for query results. It works as an extension to the second-level cache and does not replace it. What is cached: The result of a query (JPQL/Criteria) with a specific set of parameters. Only the identifiers of the found entities and scalar values are cached, not the state of the entities themselves.
- Dependency: Since the query cache stores only IDs, Hibernate will reach out to the second-level cache to fetch the actual entity data. If the entity is not there, a database query will be executed. Therefore, the query cache is always used in tandem with the second-level cache.
- How to enable:
- Activate globally:
hibernate.cache.use_query_cache=true. - For each specific query, call
.setCacheable(true).
- Activate globally:
- Invalidation: The query cache is highly sensitive to changes. Any update to a table involved in the query invalidates the corresponding cached results. Hibernate tracks this via the
UpdateTimestampsCacheregion.
Pitfalls and Common Mistakes
- Expecting the second-level cache to work "out of the box". This is the most common mistake. Enabling the global setting is completely useless if the entities lack the
@Cacheannotation. - Caching "volatile" data. If an entity is frequently updated, the cache will constantly invalidate, creating overhead without providing any performance benefit. Cache only "reference" or rarely changing data.
- Cluster consistency issues. When using multiple application instances, a standard second-level cache (e.g., based on local memory) does not synchronize between them. One instance might update data, while another continues to serve stale data from its local cache. The solution is a distributed cache (Redis, Infinispan, Hazelcast) that synchronizes invalidation across nodes.
- Using
READ_WRITEwithout lock support. TheREAD_WRITEstrategy requires the cache provider to support locking. Not all providers can do this, and the provider's documentation usually contains explicit warnings about it. - Query cache overhead. The query cache only makes sense for frequently repeated queries with the exact same parameters. For ad-hoc or unique queries, it creates unnecessary invalidation overhead.
Summary
- First-level cache is always on, lives within a session, and ensures identity and dirty checking. Manage it via
evict/clearduring batch processing. - Second-level cache is shared across sessions but off by default. It requires a provider and an explicit
@Cacheannotation on each entity. Choose the strategy (READ_ONLY,READ_WRITE, etc.) based on the data's update frequency. - Query cache caches only the IDs of results for repetitive queries. It always works in tandem with the second-level cache; otherwise, it is pointless.
- Caching is a trade-off between read speed and the risk of serving stale data. Do not cache everything indiscriminately; start with
READ_ONLYfor reference tables. - In a cluster, always use a distributed cache (Redis/Infinispan), otherwise data consistency will be compromised.
Top comments (0)