Ehcache 3.1+ jar no longer contains the transaction related code. This is now available through a different binary:

<groupId>org.ehcache</groupId>
<artifactId>ehcache-transactions</artifactId>
Ehcache 3.1 added clustering support but this is not yet compatible with transactional caches.

What is supported and what are the limitations?

Ehcache supports caches that work within the context of an XA transaction controlled by a Java Transaction API (JTA) transaction manager. Within this context, Ehcache supports the two-phase commit protocol, including crash recovery.

  • Bitronix Transaction Manager 2.1.4, which is an open source project hosted on GitHub, is the only tested transaction manager. Other transaction managers may work but have not yet been tested.

  • Read-Committed is the only supported isolation level.

  • The isolation level is guaranteed by the use of the Copier mechanism. When no copiers are configured for either the key or the value, default ones are automatically used instead. You cannot disable the Copier mechanism for a transactional cache.

  • Accessing a cache outside of a JTA transaction context is forbidden.

  • There is no protection against the ABA problem.

  • Everything else works orthogonally.

Configuring it all in Java

The simplest case

The simplest possible configuration is to configure a cache manager as transactionally aware by using the provided Bitronix transaction manager integration.

This INFO level log entry informs you of the detected transaction manager:

INFO org.ehcache.transactions.xa.txmgr.btm.BitronixTransactionManagerLookup - Using looked up transaction manager : a BitronixTransactionManager with 0 in-flight transaction(s)

Here is an example:

BitronixTransactionManager transactionManager =
    TransactionManagerServices.getTransactionManager(); (1)

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    .using(new LookupTransactionManagerProviderConfiguration(BitronixTransactionManagerLookup.class)) (2)
    .withCache("xaCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, (3)
                                                        ResourcePoolsBuilder.heap(10)) (4)
        .withService(new XAStoreConfiguration("xaCache")) (5)
        .build()
    )
    .build(true);

Cache<Long, String> xaCache = cacheManager.getCache("xaCache", Long.class, String.class);

transactionManager.begin(); (6)
{
  xaCache.put(1L, "one"); (7)
}
transactionManager.commit(); (8)

cacheManager.close();
transactionManager.shutdown();
1 First start the Bitronix transaction manager. By default, Ehcache will auto-detect it but will throw an exception during the cache manager initialization if BTM isn’t started.
2 Configure the cache manager such as it can handle transactions by having a TransactionManagerProvider loaded and configured to use Bitronix.
3 Register a cache the normal way.
4 Give it the resources you wish.
5 Add a XAStoreConfiguration object to make the cache XA transactional. You must also give the cache a unique XAResource identifier as some transaction managers require this.
6 Begin a JTA transaction the normal way.
7 Work with the cache the normal way, all operations are supported. Note that concurrent transactions will not see those pending changes.
8 Commit the JTA transaction. Other transactions can now see the changes you made to the cache.

Configuring your transaction manager

While only the Bitronix JTA implementation has been tested so far, plugging-in another one is possible.

You will need to implement a org.ehcache.transactions.xa.txmgr.provider.TransactionManagerLookup and make sure you understand its expected lifecycle as well as the one of the org.ehcache.transactions.xa.txmgr.provider.LookupTransactionManagerProvider.

If such a lifecycle does not match your needs, you will have to go one step further and implement your own org.ehcache.transactions.xa.txmgr.provider.TransactionManagerProvider.

XA write-through cache

When a XA cache is configured in write-though mode, the targeted SoR will automatically participate in the JTA transaction context. Nothing special needs to be configured for this to happen, just ensure that the configured CacheLoaderWriter is configured to work with XA transactions.

BitronixTransactionManager transactionManager =
    TransactionManagerServices.getTransactionManager(); (1)

Class<CacheLoaderWriter<?, ?>> klazz = (Class<CacheLoaderWriter<?, ?>>) (Class) (SampleLoaderWriter.class);

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    .using(new LookupTransactionManagerProviderConfiguration(BitronixTransactionManagerLookup.class)) (2)
    .withCache("xaCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, (3)
                                                        ResourcePoolsBuilder.heap(10)) (4)
            .withService(new XAStoreConfiguration("xaCache")) (5)
            .withService(new DefaultCacheLoaderWriterConfiguration(klazz, singletonMap(1L, "eins"))) (6)
            .build()
    )
    .build(true);

Cache<Long, String> xaCache = cacheManager.getCache("xaCache", Long.class, String.class);

transactionManager.begin(); (7)
{
  assertThat(xaCache.get(1L), equalTo("eins")); (8)
  xaCache.put(1L, "one"); (9)
}
transactionManager.commit(); (10)

cacheManager.close();
transactionManager.shutdown();
1 First start the Bitronix transaction manager. By default, Ehcache will auto-detect it but will throw an exception during the cache manager initialization if BTM isn’t started.
2 Configure the cache manager such as it can handle transactions by having a TransactionManagerProvider loaded and configured to use Bitronix.
3 Register a cache the normal way.
4 Give it the resources you wish.
5 Add a XAStoreConfiguration object to make the cache XA transactional. You must also give the cache a unique XAResource identifier as some transaction managers require this.
6 Add a CacheLoaderWriter configuration. This one is a mocked SoR backed by a map for illustration purpose that is filled with {1L,"eins"} key/value pair at startup.
7 Begin a JTA transaction the normal way.
8 The cache is empty at startup, so the CacheLoaderWriter will be called to load the value.
9 Update the value. This will make the CacheLoaderWriter write to the SoR.
10 Commit the JTA transaction. Other transactions can now see the changes you made to the cache and the SoR.

Transactional scope

A XA cache can only be accessed within a JTA transaction’s context. Any attempt to access one outside of such context will result in XACacheException to be thrown.

BitronixTransactionManager transactionManager =
    TransactionManagerServices.getTransactionManager(); (1)

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    .using(new LookupTransactionManagerProviderConfiguration(BitronixTransactionManagerLookup.class)) (2)
    .withCache("xaCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, (3)
                                                        ResourcePoolsBuilder.heap(10)) (4)
        .withService(new XAStoreConfiguration("xaCache")) (5)
        .build()
    )
    .build(true);

Cache<Long, String> xaCache = cacheManager.getCache("xaCache", Long.class, String.class);

try {
  xaCache.get(1L); (6)
  fail("expected XACacheException");
} catch (XACacheException e) {
  // expected
}

cacheManager.close();
transactionManager.shutdown();
1 First start the Bitronix transaction manager. By default, Ehcache 3 will auto-detect it but will throw an exception during the cache manager initialization if BTM isn’t started.
2 Configure the cache manager such as it can handle transactions by having a TransactionManagerProvider loaded and configured to use Bitronix.
3 Register a cache the normal way.
4 Give it the resources you wish.
5 Add a XAStoreConfiguration object to make the cache XA transactional. You must also give the cache a unique XAResource identifier as some transaction managers require this.
6 The cache is being accessed with no prior call to transactionManager.begin() which makes it throw XACacheException.
There is one exception to that rule: the Cache.clear() method will always wipe the cache’s contents non-transactionally.

XA cache with three tiers and persistence

When a cache is configured as persistent, the in-doubt transactions are preserved and can be recovered across restarts.

This INFO log informs you about that in-doubt transactions journaling is persistent too:

INFO o.e.t.x.j.DefaultJournalProvider - Using persistent XAStore journal

Here is an example:

BitronixTransactionManager transactionManager =
    TransactionManagerServices.getTransactionManager(); (1)

PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    .using(new LookupTransactionManagerProviderConfiguration(BitronixTransactionManagerLookup.class)) (2)
    .with(CacheManagerBuilder.persistence(new File(getStoragePath(), "testXACacheWithThreeTiers"))) (3)
    .withCache("xaCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, (4)
            ResourcePoolsBuilder.newResourcePoolsBuilder() (5)
                    .heap(10, EntryUnit.ENTRIES)
                    .offheap(10, MemoryUnit.MB)
                    .disk(20, MemoryUnit.MB, true)
            )
            .withService(new XAStoreConfiguration("xaCache")) (6)
            .build()
    )
    .build(true);

Cache<Long, String> xaCache = persistentCacheManager.getCache("xaCache", Long.class, String.class);

transactionManager.begin(); (7)
{
  xaCache.put(1L, "one"); (8)
}
transactionManager.commit(); (9)

persistentCacheManager.close();
transactionManager.shutdown();
1 First start the Bitronix transaction manager. By default, Ehcache 3 will auto-detect it but will throw an exception during the cache manager initialization if BTM isn’t started.
2 Configure the cache manager such as it can handle transactions by having a TransactionManagerProvider loaded and configured to use Bitronix.
3 Configure persistence support to enable the use of the disk tier.
4 Register a cache the normal way.
5 Give it the resources you want.
6 Add a XAStoreConfiguration object to make the cache XA transactional. You must also give the cache a unique XAResource identifier as some transaction managers require this.
7 Begin a JTA transaction the normal way.
8 Update the value.
9 Commit the JTA transaction. Other transactions can now see the changes you made to the cache and the SoR.

Configuring it with XML

You can create a XML file to configure a CacheManager, lookup a specific transaction manager and configure XA caches:

<service>
  <tx:jta-tm transaction-manager-lookup-class="org.ehcache.transactions.xa.txmgr.btm.BitronixTransactionManagerLookup"/> (1)
</service>

<cache alias="xaCache"> (2)
  <key-type>java.lang.String</key-type>
  <value-type>java.lang.String</value-type>
  <heap unit="entries">20</heap>
  <tx:xa-store unique-XAResource-id="xaCache" /> (3)
</cache>
1 Declare a TransactionManagerLookup that will lookup your transaction manager.
2 Configure a xaCache cache the normal way.
3 Configure xaCache as an XA cache, giving it xaCache as its unique XAResource ID.

In order to parse an XML configuration, you can use the XmlConfiguration type:

BitronixTransactionManager transactionManager =
    TransactionManagerServices.getTransactionManager(); (1)

URL myUrl = this.getClass().getResource("/docs/configs/xa-getting-started.xml"); (2)
Configuration xmlConfig = new XmlConfiguration(myUrl); (3)
CacheManager myCacheManager = CacheManagerBuilder.newCacheManager(xmlConfig); (4)
myCacheManager.init();

myCacheManager.close();
transactionManager.shutdown();
1 The Bitronix transaction manager must be started before the cache manager is initialized.
2 Create a URL to your XML file’s location.
3 Instantiate a XmlConfiguration passing it the XML file’s URL.
4 Using the static org.ehcache.config.builders.CacheManagerBuilder.newCacheManager(org.ehcache.config.Configuration) lets you create your CacheManager instance using the Configuration from the XmlConfiguration.

And here is what the BitronixTransactionManagerLookup implementation looks like:

public class BitronixTransactionManagerLookup implements TransactionManagerLookup<TransactionManager> { (1)

  private static final Logger LOGGER = LoggerFactory.getLogger(BitronixTransactionManagerLookup.class);

  @Override
  public TransactionManagerWrapper<TransactionManager> lookupTransactionManagerWrapper() { (2)
    if (!TransactionManagerServices.isTransactionManagerRunning()) { (3)
      throw new IllegalStateException("BTM must be started beforehand");
    }
    TransactionManagerWrapper<TransactionManager> tmWrapper = new TransactionManagerWrapper<>(TransactionManagerServices.getTransactionManager(),
        new BitronixXAResourceRegistry()); (4)
    LOGGER.info("Using looked up transaction manager : {}", tmWrapper);
    return tmWrapper;
  }
}
1 The TransactionManagerLookup interface must be implemented and the offer a no-arg constructor.
2 The lookupTransactionManagerWrapper() method must return a TransactionManagerWrapper instance.
3 Here is the check that makes sure BTM is started.
4 The TransactionManagerWrapper class is constructed with both the javax.transaction.TransactionManager instance as well as a XAResourceRegistryinstance. The latter is used to register the javax.transaction.xa.XAResource instances of the cache with the transaction manager using an implementation-specific mechanism. If your JTA implementation doesn’t require that, you can use the NullXAResourceRegistryinstead.