From the author of

From the author of

Atomicity

The third reason listed for using transactions is atomicity. Consider
an application suite in which multiple threads of control (multiple processes
or threads in one or more processes) are changing the values associated with
a key in one or more databases. Specifically, they are taking the current value,
incrementing it, and then storing it back into the database.

Such an application requires atomicity. Because we want to change a value in
the database, we must make sure that after we read it, no other thread of control
modifies it. For example, assume that both thread #1 and thread #2 are doing
similar operations in the database, where thread #1 is incrementing records
by 3, and thread #2 is incrementing records by 5. We want to increment the record
by a total of 8. If the operations interleave in the right (well, wrong) order,
that is not what will happen:

As you can see, instead of incrementing the record by a total of 8, we've incremented
it only by 3 because thread #1 overwrote thread #2's change. By wrapping the
operations in transactions, we ensure that this cannot happen. In a transaction,
when the first thread reads the record, locks are acquired that will not be
released until the transaction finishes, guaranteeing that all other readers
and writers will block, waiting for the first thread's transaction to complete
(or to be aborted).

Here is an example function that does transaction-protected increments on database
records to ensure atomicity:

Any number of operations on any number of databases can be included in a single
transaction to ensure the atomicity of the operations. There is, however, a
trade-off between the number of operations included in a single transaction
and both throughput and the possibility of deadlock. The reason for this is
because transactions acquire locks throughout their lifetime, and do not release
the locks until commit or abort time. So, the more operations included in a
transaction, the more likely it is that a transaction will block other operations
and that deadlock will occur. However, each transaction commit requires a synchronous
disk I/O, so grouping multiple operations into a transaction can increase overall
throughput. (There is one exception to this. The DB_TXN_NOSYNC option causes
transactions to exhibit the ACI (atomicity, consistency and isolation) properties,
but not D (durability); avoiding the synchronous disk I/O on transaction commit
and greatly increasing transaction throughput for some applications.

When applications do create complex transactions, they often avoid having more
than one complex transaction at a time because simple operations like a single
DB->put are unlikely to deadlock with each other or the complex transaction;
while multiple complex transactions are likely to deadlock with each other because
they will both acquire many locks over their lifetime. Alternatively, complex
transactions can be broken up into smaller sets of operations, and each of those
sets may be encapsulated in a nested transaction. Because nested transactions
may be individually aborted and retried without causing the entire transaction
to be aborted, this allows complex transactions to proceed even in the face
of heavy contention, repeatedly trying the suboperations until they succeed.

It is also helpful to order operations within a transaction; that is, access
the databases and items within the databases in the same order, to the extent
possible, in all transactions. Accessing databases and items in different orders
greatly increases the likelihood of operations being blocked and failing due
to deadlocks.