JBoss.orgCommunity Documentation

Chapter 10. Transactions and Concurrency

10.1. Concurrent Access
10.1.1. Locks
10.1.2. Pessimistic locking
10.1.3. Optimistic Locking
10.2. Transactional Support

JBoss Cache is a thread safe caching API, and uses its own efficient mechanisms of controlling concurrent access. It uses a pessimistic locking scheme by default for this purpose. Optimistic locking may alternatively be used, and is discussed later.

Locking is done internally, on a node-level. For example when we want to access "/a/b/c", a lock will be acquired for nodes "a", "b" and "c". When the same transaction wants to access "/a/b/c/d", since we already hold locks for "a", "b" and "c", we only need to acquire a lock for "d".

Lock owners are either transactions (call is made within the scope of an existing transaction) or threads (no transaction associated with the call). Regardless, a transaction or a thread is internally transformed into an instance of GlobalTransaction , which is used as a globally unique identifier for modifications across a cluster. E.g. when we run a two-phase commit protocol across the cluster, the GlobalTransaction uniquely identifies a unit of work across a cluster.

Locks can be read or write locks. Write locks serialize read and write access, whereas read-only locks only serialize read access. When a write lock is held, no other write or read locks can be acquired. When a read lock is held, others can acquire read locks. However, to acquire write locks, one has to wait until all read locks have been released. When scheduled concurrently, write locks always have precedence over read locks. Note that (if enabled) read locks can be upgraded to write locks.

Using read-write locks helps in the following scenario: consider a tree with entries "/a/b/n1" and "/a/b/n2". With write-locks, when Tx1 accesses "/a/b/n1", Tx2 cannot access "/a/b/n2" until Tx1 has completed and released its locks. However, with read-write locks this is possible, because Tx1 acquires read-locks for "/a/b" and a read-write lock for "/a/b/n1". Tx2 is then able to acquire read-locks for "/a/b" as well, plus a read-write lock for "/a/b/n2". This allows for more concurrency in accessing the cache.

By default, JBoss Cache uses pessimistic locking. Locking is not exposed directly to user. Instead, a transaction isolation level which provides different locking behaviour is configurable.

JBoss Cache supports the following transaction isolation levels, analogous to database ACID isolation levels. A user can configure an instance-wide isolation level of NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, or SERIALIZABLE. REPEATABLE_READ is the default isolation level used.

  1. NONE. No transaction support is needed. There is no locking at this level, e.g., users will have to manage the data integrity. Implementations use no locks.

  2. READ_UNCOMMITTED. Data can be read anytime while write operations are exclusive. Note that this level doesn't prevent the so-called "dirty read" where data modified in Tx1 can be read in Tx2 before Tx1 commits. In other words, if you have the following sequence,

       Tx1     Tx2
        W
                R
    

    using this isolation level will not prevent Tx2 read operation. Implementations typically use an exclusive lock for writes while reads don't need to acquire a lock.

  3. READ_COMMITTED. Data can be read any time as long as there is no write. This level prevents the dirty read. But it doesn’t prevent the so-called ‘non-repeatable read’ where one thread reads the data twice can produce different results. For example, if you have the following sequence,

       Tx1     Tx2
        R
                W
        R
    

    where the second read in Tx1 thread will produce different result.

    Implementations usually use a read-write lock; reads succeed acquiring the lock when there are only reads, writes have to wait until there are no more readers holding the lock, and readers are blocked acquiring the lock until there are no more writers holding the lock. Reads typically release the read-lock when done, so that a subsequent read to the same data has to re-acquire a read-lock; this leads to nonrepeatable reads, where 2 reads of the same data might return different values. Note that, the write only applies regardless of transaction state (whether it has been committed or not).

  4. REPEATABLE_READ. Data can be read while there is no write and vice versa. This level prevents "non-repeatable read" but it does not completely prevent the so-called "phantom read" where new data can be inserted into the tree from another transaction. Implementations typically use a read-write lock. This is the default isolation level used.

  5. SERIALIZABLE. Data access is synchronized with exclusive locks. Only 1 writer or reader can have the lock at any given time. Locks are released at the end of the transaction. Regarded as very poor for performance and thread/transaction concurrency.

The motivation for optimistic locking is to improve concurrency. When a lot of threads have a lot of contention for access to the data tree, it can be inefficient to lock portions of the tree - for reading or writing - for the entire duration of a transaction as we do in pessimistic locking. Optimistic locking allows for greater concurrency of threads and transactions by using a technique called data versioning, explained here. Note that isolation levels (if configured) are ignored if optimistic locking is enabled.

Optimistic locking treats all method calls as transactional [7] . Even if you do not invoke a call within the scope of an ongoing transaction, JBoss Cache creates an implicit transaction and commits this transaction when the invocation completes. Each transaction maintains a transaction workspace, which contains a copy of the data used within the transaction.

For example, if a transaction calls cache.getRoot().getChild( Fqn.fromString("/a/b/c") ) , nodes a, b and c are copied from the main data tree and into the workspace. The data is versioned and all calls in the transaction work on the copy of the data rather than the actual data. When the transaction commits, its workspace is merged back into the underlying tree by matching versions. If there is a version mismatch - such as when the actual data tree has a higher version than the workspace, perhaps if another transaction were to access the same data, change it and commit before the first transaction can finish - the transaction throws a RollbackException when committing and the commit fails.

Optimistic locking uses the same locks we speak of above, but the locks are only held for a very short duration - at the start of a transaction to build a workspace, and when the transaction commits and has to merge data back into the tree.

So while optimistic locking may occasionally fail if version validations fail or may run slightly slower than pessimistic locking due to the inevitable overhead and extra processing of maintaining workspaces, versioned data and validating on commit, it does buy you a near-SERIALIZABLE degree of data integrity while maintaining a very high level of concurrency.

JBoss Cache can be configured to use and participate in JTA compliant transactions. Alternatively, if transaction support is disabled, it is equivalent to setting AutoCommit to on where modifications are potentially [9] replicated after every change (if replication is enabled).

What JBoss Cache does on every incoming call is:

  1. Retrieve the current javax.transaction.Transaction associated with the thread

  2. If not already done, register a javax.transaction.Synchronization with the transaction manager to be notified when a transaction commits or is rolled back.

In order to do this, the cache has to be provided with a reference to environment's javax.transaction.TransactionManager . This is usually done by configuring the cache with the class name of an implementation of the TransactionManagerLookup interface. When the cache starts, it will create an instance of this class and invoke its getTransactionManager() method, which returns a reference to the TransactionManager .

JBoss Cache ships with JBossTransactionManagerLookup and GenericTransactionManagerLookup . The JBossTransactionManagerLookup is able to bind to a running JBoss AS instance and retrieve a TransactionManager while the GenericTransactionManagerLookup is able to bind to most popular Java EE application servers and provide the same functionality. A dummy implementation - DummyTransactionManagerLookup - is also provided, primarily for unit tests. Being a dummy, this is just for demo and testing purposes and is not recommended for production use.

An alternative to configuring a TransactionManagerLookup is to programatically inject a reference to the TransactionManager into the Configuration object's RuntimeConfig element:



   TransactionManager tm = getTransactionManager(); // magic method
   cache.getConfiguration().getRuntimeConfig().setTransactionManager(tm);
      

Injecting the TransactionManager is the recommended approach when the Configuration is built by some sort of IOC container that already has a reference to the TM.

When the transaction commits, we initiate either a one- two-phase commit protocol. See replicated caches and transactions for details.



[7] Because of this requirement, you must always have a transaction manager configured when using optimistic locking.

[9] Depending on whether interval-based asynchronous replication is used