Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly.
Hypersistence Optimizer is that tool!
Introduction
In my previous post, I introduced the READ_WRITE second-level cache concurrency mechanism. In this article, I am going to continue this topic with the TRANSACTIONAL strategy.
Write-through caching
While the READ_WRITE CacheConcurrencyStartegy is an asynchronous write-through caching mechanism (since changes are being propagated only after the current database transaction is completed), the TRANSACTIONAL CacheConcurrencyStartegy is synchronized with the current XA transaction.
To enlist two sources of data (the database and the second-level cache) in the same global transaction, we need to use the Java Transaction API and a JTA transaction manager must coordinate the participating XA resources.
In this mode, the second-level cache exposes an XAResource interface, so it can participate in the two-phase commit (2PC) protocol.
The entity state is modified both in the database and in the cache, but these changes are isolated from other concurrent transactions and they become visible once the current XA transaction gets committed.
The database and the cache remain consistent even in case of an application crash.
xa
If only one data source participates in a globaltransaction, the transaction manager can apply the one-phase commit optimization. The second-level cache is managed through a Synchronization transaction callback. The second-level cache doesn’t actively participates in deciding the transaction outcome, as it merely executes according to the current database transaction outcome:
This mode trades durability for latency and in case of a server crash (happening in between the database transaction commit and the second-level cache transaction callback), the two data sources will drift apart. This issue can be mitigated if our entities employ an optimistic concurrency control mechanism, so even if we read stale data, we will not lose updates upon writing.
Isolation level
To the validate the TRANSACTIONAL concurrency strategy isolation level, we are going to use the following test case:
Alice loads a Repository entity into its current Persistence Context
Bob loads the same Repository and then modifies it
After Bob’s transaction is committed, Alice still sees the old Repository data, because the Persistence Context provides application-level repeatable reads
When Alice evicts the Repository from the first-level cache and fetches it anew, she will see Bob’s changes
The second-level cache doesn’t offer repeatable reads guarantees since the first-level cache already does this anyway.
Next, we’ll investigate if dirty reads or lost updates are possible and for this we are going to use the following test:
This test will emulate two concurrent transactions, trying to update the same Repository entity. This use case is run on PostgreSQL, using the default READ_COMMITTED transaction isolation level.
Running this test generates the following output:
Alice loads the Repository entity
[Alice]: n.s.e.TransactionController - begun transaction 4
[Alice]: n.s.e.t.l.LocalTransactionStore - get: cache [com.vladmihalcea.hibernate.model.cache.Repository] key [com.vladmihalcea.hibernate.model.cache.Repository#11] not soft locked, returning underlying element
Alice changes the Repository name
Alice flushes the current Persistent Context, so an UPDATE statement is executed. Because Alice’s transaction has not yet committed, a lock will prevent other concurrent transactions from modifying the same Repository row
[Alice]: n.t.d.l.CommonsQueryLoggingListener - Name:, Time:1, Num:1, Query:{[update repository set name=? where id=?][High-Performance Hibernate,11]}
[Alice]: n.s.e.t.l.LocalTransactionStore - put: cache [com.vladmihalcea.hibernate.model.cache.Repository] key [com.vladmihalcea.hibernate.model.cache.Repository#11] was in, replaced with soft lock
Bob starts a new transaction and loads the same Repository entity
Alice thread allows Bob’s thread to continue and she starts waiting on the bobLatch for Bob to finish his transaction
Bob can simply issue a database UPDATE and a second-level cache entry modification, without noticing that Alice has changed the Repository since he first loaded it
After Bob manages to update the Repository database and cache records, Alice starts a new transaction and she can see Bob’s changes
[Alice]: c.v.HibernateCacheTest - Reload entity after Bob's update
[Alice]: o.h.e.t.i.TransactionCoordinatorImpl - Skipping JTA sync registration due to auto join checking
[Alice]: o.h.e.t.i.TransactionCoordinatorImpl - successfully registered Synchronization
[Alice]: n.s.e.TransactionController - begun transaction 7
[Alice]: n.s.e.t.l.LocalTransactionStore - get: cache [com.vladmihalcea.hibernate.model.cache.Repository] key [com.vladmihalcea.hibernate.model.cache.Repository#11] not soft locked, returning underlying element
WARN [Alice]: b.t.t.Preparer - executing transaction with 0 enlisted resource
[Alice]: n.s.e.t.l.LocalTransactionContext - 0 participating cache(s), committing transaction 7
[Alice]: n.s.e.t.l.LocalTransactionContext - committed transaction 7
[Alice]: n.s.e.t.l.LocalTransactionContext - unfreezing and unlocking 0 soft lock(s)
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The TRANSACTIONAL CacheConcurrencyStrategy employes a READ_COMMITTED transaction isolation, preventing dirty reads while still allowing the lost updates phenomena. Adding optimistic locking can eliminate the lost update anomaly since the database transaction will rollback on version mismatches. Once the database transaction fails, the current XA transaction is rolled back, causing the cache to discard all uncommitted changes.
If the READ_WRITE concurrency strategy implies less overhead, the TRANSACTIONAL synchronization mechanism is appealing for higher write-read ratios (requiring fewer database hits compared to its READ_WRITE counterpart). The inherent performance penalty must be compared against the READ_WRITE extra database access when deciding which mode is more suitable for a given data access pattern.
Based on my book, High-Performance Java Persistence, this workshop teaches you various data access performance optimizations from JDBC, to JPA, Hibernate and jOOQ for the major rational database systems (e.g. Oracle, SQL Server, MySQL and PostgreSQL).