Transactions and Concurrency

Multi-threaded transactional systems measure the protection that they offer to the data accessed by their threads of control in terms of degrees of isolation. In general, a transaction system can support up to 4 degrees of isolation. Briefly:

By default, JE transactions offer degree 3 isolation. You can optionally configure JE to use degree 1 isolation by configuring JE to to perform dirty reads. See Configuring Dirty Reads for more information.

Transactions and Deadlocks

Transactions acquire locks on database records throughout their lifetimes, and they do not release those locks until commit or abort time. This is how JE provides isolation for its transactions. When a transaction locks a record for write access, no other transaction can access that record for write (and by default for read) until the lock is released. When a record is locked for read access, no other transaction can lock it for write access.

The result of this locking activity is that two threads of control can deadlock – that is, attempt to simultaneously lock the same record. When this happens, a DeadlockException is thrown for one of the deadlocked threads.

When a thread catches a DeadlockException, then that thread must release its locks in order to resolve the deadlock. The thread releases its locks by closing any cursors involved in the transaction and then aborting the transaction. The thread may then optionally begin a new transaction and retry the operation that it just aborted.

Performance Considerations

Any number of operations on any number of Database handles can be included in a single transaction. When many operations are grouped together in a transaction, then that is considered to be a complex transaction. There is a trade-off between the number of operations included in a complex transaction and your application's throughput as well as the possibility of deadlock.

Because transactions acquire locks throughout their lifetimes, the likelihood of a deadlock occurring increases as the number of operations performed by a transaction increases. The likelihood of deadlock occurring also increases as the number of threads performing database operations increases. If your transactions become complex enough and the number of threads operating on your databases increases high enough, your application can find itself spending more time resolving deadlocks that it does performing useful work.

Note

JE applications will only see deadlocks when multiple transactions attempt simultaneous access of the same database records. JE performs record-level locking only. You can have multiple simultaneous complex transactions without any deadlock concerns so long as the number of records simultaneously accessed by those transactions is small.

On the other hand, a transaction commit usually results in synchronous disk I/O (this is not true for Transaction.commitNoSync() – see Committing and Aborting Transactions for details). As a result, having longer-lived transactions or more operations in a transaction can improve your application's performance by avoiding disk I/O.

Obviously you will have to study the workload expected of your application in order to decide on how to best resolve the trade-off between reduced disk I/O and the potential for deadlocks. Consider the following as you study this problem:

  • If you do decide to use complex transactions, then try to avoid running multiple complex transactions that perform simultaneous access of the same database records. Instead, try to organize your transactions so that they do not overlap in the records that they want to access. If this is not feasible, then limit yourself to a small number of threads running complex transactions so as to avoid deadlock problems. How many threads you can have accessing overlapping sets of database records will depend on the length and complexity of your transactions. Ultimately, only performance and stress testing can help you determine the mixture of numbers of threads versus transactional complexity that is appropriate for your application.

  • Try to access your Database handles, and the records in your databases, in the same order for all transactions. Accessing databases and records in different order in multiple transactions greatly increases the likelihood of deadlocks.

  • Most likely your application will have at least one (and probably many) threads that perform read-only operations. You should avoid using transactions for operations that just perform reads as transactionally protecting read-only operations can cause performance problems. For example, a transactionally protected cursor walking your database will eventually lock all of the records in your database. In this situation, your other threads have to wait until the read-only transaction completes before they can obtain a lock for their own operations.

    Note, however, that read-only operations occurring in an application with one or more threads performing writes should be prepared to catch and respond to deadlock exceptions. By default read-only operations lock records that they are reading for the duration of that read. The exception to this is if you are performing dirty reads. See Configuring Dirty Reads for more information.

    Also, if your read operations are not transactionally protected, then there is no guarantee as to the stability of the records read in the database. Repeatedly reading the same record can cause different data to return if there are other threads writing and committing changes to the database. If your read operations require stability for their reads, then you must transactionally protect them.