Version 2.3.1
Transactions

ACID properties

Transactions provide a powerful abstraction for multiple threads to operate on data concurrently because they have the following properties:

  • Atomicity: all or none of a transaction is completed.
  • Consistency: if each transaction maintains some property when considered separately, then the combined effect of executing the transactions concurrently will maintain the same property.
  • Isolation: developers can reason about transactions as if they run single-threaded.
  • Durability: once a transaction commits, its updates cannot be lost.

WiredTiger supports transactions with the following caveats to the ACID properties:

  • the maximum level of isolation supported is snapshot isolation. See Isolation levels for more details.
  • only coarse-grained durability is supported: updates become durable when they are part of a checkpoint, not at commit time. If there is a crash, commits since the last checkpoint will be lost.

Transactional API

In WiredTiger, transaction operations are methods off the WT_SESSION class.

Applications call WT_SESSION::begin_transaction to start a new transaction. Operations subsequently performed using that WT_SESSION handle, including operations on any cursors open in that WT_SESSION handle, are part of the transaction and their effects committed by calling WT_SESSION::commit_transaction, or discarded by calling WT_SESSION::rollback_transaction.

If WT_SESSION::commit_transaction returns an error for any reason, the transaction was rolled-back, not committed.

When transactions are used, data operations can encounter a conflict and fail with the WT_DEADLOCK error. If this error occurs, transactions should be rolled back with WT_SESSION::rollback_transaction and retried.

The WT_SESSION::begin_transaction, WT_SESSION::commit_transaction and WT_SESSION::rollback_transaction methods all implicitly reset all open cursors in the WT_SESSION, as if WT_CURSOR::reset were called, discarding any position or key/value information they may have.

ret =
session->open_cursor(session, "table:mytable", NULL, NULL, &cursor);
ret = session->begin_transaction(session, NULL);
/*
* Cursors may be opened before or after the transaction begins, and in
* either case, subsequent operations are included in the transaction.
* The begin_transaction call resets all open cursors.
*/
cursor->set_key(cursor, "key");
cursor->set_value(cursor, "value");
switch (ret = cursor->update(cursor)) {
case 0: /* Update success */
ret = session->commit_transaction(session, NULL);
/*
* The commit_transaction call resets all open cursors.
* If commit_transaction fails, the transaction was rolled-back.
*/
break;
case WT_DEADLOCK: /* Update conflict */
default: /* Other error */
ret = session->rollback_transaction(session, NULL);
/* The rollback_transaction call resets all open cursors. */
break;
}
/* Cursors remain open and may be used for multiple transactions. */

Implicit transactions

If a cursor is used when no explicit transaction is active in a session, reads are performed at the isolation level of the session, set with the isolation key to WT_CONNECTION::open_session, and successful updates are automatically committed before the update operation returns.

Any operation consisting of multiple related updates should be enclosed in an explicit transaction to ensure the updates are applied atomically.

If an implicit transaction successfully commits, the open cursors in the WT_SESSION remain open and positioned. If an implicit transaction fails, all open cursors in the WT_SESSION are reset, as if WT_CURSOR::reset were called, discarding any position or key/value information they may have.

See Cursors and Transactions for more information.

Concurrency control

WiredTiger uses optimistic concurrency control algorithms. This avoids the bottleneck of a centralized lock manager and ensures transactional operations do not block: reads do not block writes, and vice versa.

Further, writes do not block writes, although concurrent transactions updating the same value will fail with WT_DEADLOCK. Some applications may benefit from application-level synchronization to avoid repeated attempts to rollback and update the same value.

Operations in transactions may also fail with the WT_DEADLOCK error if some resource cannot be allocated after repeated attempts. For example, if the cache is not large enough to hold the updates required to satisfy transactional readers, an operation may fail and return WT_DEADLOCK.

Isolation levels

WiredTiger supports read-uncommitted, read-committed and snapshot isolation levels; the default isolation level is read-committed.

  • read-uncommitted: Transactions can see changes made by other transactions before those transactions are committed. Dirty reads, non-repeatable reads and phantoms are possible.
  • read-committed: Transactions cannot see changes made by other transactions before those transactions are committed. Dirty reads are not possible; non-repeatable reads and phantoms are possible. Committed changes from concurrent transactions become visible when no cursor is positioned in the read-committed transaction.
  • snapshot: Transactions read the versions of records committed before the transaction started. Dirty reads and non-repeatable reads are not possible; phantoms are possible.

    Snapshot isolation is a strong guarantee, but not equivalent to a single-threaded execution of the transactions, known as serializable isolation. Concurrent transactions T1 and T2 running under snapshot isolation may both commit and produce a state that neither (T1 followed by T2) nor (T2 followed by T1) could have produced, if there is overlap between T1's reads and T2's writes, and between T1's writes and T2's reads.

The transaction isolation level can be configured on a per-transaction basis:

/* A single transaction configured for snapshot isolation. */
ret =
session->open_cursor(session, "table:mytable", NULL, NULL, &cursor);
ret = session->begin_transaction(session, "isolation=snapshot");
cursor->set_key(cursor, "some-key");
cursor->set_value(cursor, "some-value");
ret = cursor->update(cursor);
ret = session->commit_transaction(session, NULL);

Additionally, the default transaction isolation can be configured and re-configured on a per-session basis:

/* Open a session configured for read-uncommitted isolation. */
ret = conn->open_session(
conn, NULL, "isolation=read_uncommitted", &session);
/* Re-configure a session for snapshot isolation. */
ret = session->reconfigure(session, "isolation=snapshot");

Checkpoints and Recovery

Recovery is run automatically when a data source is opened. Any changes since the last checkpoint are discarded, and the application restarts from a consistent point in the transaction history.

This demonstrates the importance of regular checkpoints: they limit the volume of commits that may be lost in a crash. See WT_SESSION::checkpoint and Checkpoints for further information.