Using NHibernate’s Second‑Level Cache to Reduce Database Load
Learn how to enable NHibernate’s second‑level cache, pick a consistency strategy, and monitor its effectiveness with built‑in statistics.
21 Jul 2025, 01:54 UTC

Problem: Repeated Reads Hit the Database
In many line‑of‑business applications a small set of entities accounts for the majority of read traffic. Each request creates a new ISession, issues a SELECT, and waits for a round‑trip to the database. When reads dominate, the database can become a bottleneck and latency spikes during traffic bursts.
What the Second‑Level Cache Actually Does
NHibernate’s second‑level cache lives outside the per‑session cache and is shared by all sessions created from the same ISessionFactory. It can store three kinds of data:
- Entity instances – a single row mapped to a POCO.
- Collection values – the children of a one‑to‑many or many‑to‑many association.
- Query results – the rows returned by a named or HQL query.
When a session loads an entity, NHibernate first checks the shared cache. If a valid entry exists, the database query is skipped. The cache is divided into regions, each identified by a name that defaults to the entity’s fully‑qualified type name.
Enabling the Cache: Mapping + Provider
Two steps are required:
- Declare the caching strategy in the entity mapping (XML or Fluent).
- Register an
ICacheProviderimplementation and turn the second‑level cache on in the session‑factory configuration.
XML Mapping Example
<class name="Product" table="Products" cache="nonstrict-read-write">
<id name="Id" column="ProductId" generator-class="native"/>
<property name="Name" column="ProductName" not-null="true"/>
<property name="Price" column="Price" not-null="true"/>
</class>
Fluent NHibernate Alternative
public class ProductMap : ClassMap
{
public ProductMap()
{
Table("Products");
Id(x => x.Id).GeneratedBy.Native();
Map(x => x.Name).Not.Nullable();
Map(x => x.Price).Not.Nullable();
Cache.NonstrictReadWrite(); // or Cache.ReadOnly()
}
}
Registering a Provider (app.config)
The exact syntax varies between NHibernate versions; the snippet below works with NHibernate 5.x and the NHibernate.Caches.Redis package.
<hibernate-configuration>
<session-factory>
<property name="cache.provider_class">NHibernate.Caches.Redis.RedisCacheProvider, NHibernate.Caches.Redis</property>
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">true</property>
</session-factory>
</hibernate-configuration>
If you prefer appsettings.json with the generic host, you can set the same properties under the NHibernate section when building the Configuration object.
Concrete Example: Product Catalog Service
Imagine a catalog service that reads Product entities about 90 % of the time and updates them 10 % of the time. Using nonstrict-read-write gives a balance between read speed and write consistency.
- Build the session factory with the mapping and provider shown above.
- Enable NHibernate statistics to observe cache activity:
var cfg = new Configuration();
cfg.Configure(); // loads app.config/hibernate.cfg.xml
cfg.SetProperty(Environment.GenerateStatistics, "true");
var sessionFactory = cfg.BuildSessionFactory();
- Run a representative load (e.g., 10 000 reads and 1 000 writes) against the service.
- After the load, read the statistics:
var stats = sessionFactory.Statistics;
long hits = stats.EntityInsertCount + stats.EntityUpdateCount + stats.EntityDeleteCount
+ stats.EntityFetchCount + stats.CollectionFetchCount + stats.QueryExecutionCount;
// More directly, NHibernate exposes cache‑specific counters:
long cacheHits = stats.SecondLevelCacheHitCount;
long cacheMisses = stats.SecondLevelCacheMissCount;
double hitRatio = cacheMisses == 0 ? 1.0 : (double)cacheHits / (cacheHits + cacheMisses);
Console.WriteLine($"Second‑level cache hit ratio: {hitRatio:P2}");
This calculation shows the proportion of requests served from the cache versus those that missed and went to the database. The exact number will vary with your data access pattern; the purpose is to observe whether the cache is being used effectively.
Trade‑offs and Limitations
| Strategy | Consistency Guarantee | Locking Overhead | Typical Fit |
|---|---|---|---|
| Read‑Only | Always consistent (data never changes) | None | Lookup tables, static reference data |
| Nonstrict‑Read‑Write | Eventual; stale reads possible for a few seconds after a write | Low (no locks on reads) | Mostly‑read entities with occasional updates |
| Read‑Write | Immediate consistency (writes invalidate or update cache under a lock) | Higher (read‑write locks) | Data that must never be stale |
The key risk is serving stale data. If your business cannot tolerate even a short window of inconsistency, choose read-write and test lock contention under peak load. If occasional staleness is acceptable, nonstrict-read-write is usually sufficient and adds minimal overhead.
Actionable Takeaway
- Start with a
read-onlycache for immutable aggregates (e.g.,Country,Currency). - Add a
nonstrict-read-writeregion for high‑traffic, low‑write entities such asProduct. - Enable NHibernate statistics (
Environment.GenerateStatistics = true) and monitorSecondLevelCacheHitCountandSecondLevelCacheMissCountduring a realistic load test. - If the hit ratio remains low, verify that the cache region is correctly named and that the provider is reachable; consider adjusting the cache strategy or revisiting the mapping.
- When stronger consistency is required, switch to
read-writeand re‑test for lock contention under expected concurrent write loads. - Always perform writes within a transaction; writes outside a transaction may not trigger cache invalidation.
Conclusion
NHibernate’s second‑level cache can noticeably reduce database load for read‑heavy workloads, but it is not a universal speed‑up. Choose the appropriate caching strategy, validate behavior with statistics, and keep an eye on consistency guarantees. With those guardrails in place, you gain a reusable, shared cache layer that helps keep latency low without sacrificing correctness.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.