Skip to content

Create and use transactions

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

This page describes how to create and use transactions, which were introduced in Aerospike Database 8.0.0. A transaction is an encapsulation of several commands, isolated from commands outside the transaction, and executed atomically.

To ensure strict serializability, transactions in Aerospike require CP consistency mode, known in Aerospike as the strong-consistency (SC) namespace configuration. A transaction can guarantee one of two outcomes: either all commands succeed together, or one or more commands fail, in which case you can request to roll back to the state of the records prior to the attempted transaction. No commands outside the transaction can see the state changes being created inside the transaction.

Transaction best practices

  • Remember that transactions and single-record commands can be mixed in the same SC namespace.
  • Make sure to call abort when your app gives up on a transaction. Not calling abort invokes unnecessary monitor activity, and leaves records “locked” for much longer than necessary.
  • Use batch-writes to write a group of independent records in a transaction. A batch is especially efficient inside a transaction.
  • Don’t mix expiration and non-durable deletes with transactions. This is a general recommendation for strong consistency namespaces.
  • Pay attention to tombstone accumulation and tomb-raider configs.
  • Do not truncate in a namespace with active transactions. If you must use transactions, see Pause and drain transactions before truncating for steps to first disable and drain existing transactions.
  • Do not mix transactions with active-active XDR, unless you’re certain that you are not writing to the same records on both sides. Use stretch clusters (multi-site clustering) instead of XDR if you intend to use transactions this way.

Create a transaction

The following steps describe how to create a transaction, bind it to commands, and commit. A transaction object is attached to each command’s policy so the server can logically group related requests.

  1. Create a transaction.

    import com.aerospike.client.sdk.DataSet;
    DataSet demo = DataSet.of(namespace, set);
    // doInTransaction creates the transaction and runs the block in it
    session.doInTransaction(txn -> {
    System.out.println("Begin txn: " + txn.getCurrentTransaction().getId());
    // Step 2's commands go here
    });
  2. Issue commands within a try block and attach the transaction to each command’s policy.

    import com.aerospike.client.sdk.command.TxnStatus;
    TxnStatus status = session.doInTransaction(txn -> {
    // Commands on txn run in the transaction: there is no policy to set
    txn.upsert(demo.id(1))
    .bin("a").setTo("val1")
    .execute();
    });
  3. Commit the transaction.

    // There is no commit call: doInTransaction commits when the block returns,
    // and returns the outcome
    System.out.println("Commit txn: " + status);
    if (status == TxnStatus.ROLL_FORWARD_ABANDONED) {
    // Committed, but the writes stay provisional until the server rolls them forward
    }

How to handle transaction errors

When an exception is thrown during a transaction, abort the transaction to roll back all changes and release locked records.

try {
session.doInTransaction(txn -> {
txn.upsert(demo.id(1)).bin("a").setTo("val1").execute();
});
}
catch (AerospikeException ae) {
// doInTransaction has already aborted the transaction, and retried it while blocked
if (ae.getResultCode() == ResultCode.MRT_BLOCKED) {
// Transaction was still blocked after the retries — retry later
}
if (ae.getResultCode() == ResultCode.MRT_EXPIRED) {
// Transaction expired before commit or abort — retry
}
throw ae;
}

Status codes

The following status codes can be returned during a transaction:

  • MRT_BLOCKED: The transaction was blocked by another transaction accessing the same record.
  • MRT_EXPIRED: A command was sent after the transaction’s timeout.
if (ae.getResultCode() == ResultCode.MRT_BLOCKED) {
// Transaction was still blocked after doInTransaction's retries — retry later
}
if (ae.getResultCode() == ResultCode.MRT_EXPIRED) {
// Transaction expired before commit or abort — retry
}

Failed commits

A commit may fail if the read verify step fails. Aerospike attempts to do the read, even if it is dirty during the command execution step. If the read is later determined to be invalid due to a version mismatch, the commit’s verify step fails: the client aborts the transaction and reports a verify failure. This indicates that another command outside the transaction changed the targeted record. The correct way to handle this situation is to retry the entire transaction.

There is also a chance that the transaction succeeds but the commit fails. An example of this would be if there is a problem updating the transaction monitor on the server. In these situations, Aerospike returns a commit exception, and if there is uncertainty about whether the transaction was committed fully, it sets the inDoubt flag to true.

To safely handle this situation, catch the exception from the commit and implement separate handling logic as follows.

import com.aerospike.client.sdk.command.CommitError;
try {
session.doInTransaction(txn -> {
// ... the transaction's commands ...
});
}
catch (AerospikeException.Commit ce) {
// doInTransaction does not retry a failed commit, and there is no commit to call again
if (ce.getInDoubt()) {
// Commit may or may not have completed — log records for later cleanup
}
else if (ce.error == CommitError.VERIFY_FAIL) {
// Read was invalidated by an outside write — retry transaction
}
else {
// Other commit failure — retry transaction
}
}

Examples

Setting a transaction timeout

You can set a timeout, in seconds, that limits how long the commands in the transaction can run. The timeout clock starts when the first write request for the transaction is submitted; read requests do not start the timeout clock. A command sent after the timeout fails with MRT_EXPIRED, and the server rolls back a transaction that has timed out, so commit before the timeout.

session.doInTransaction(txn -> {
txn.getCurrentTransaction().setTimeout(20);
// Timeout clock starts after this operation
txn.upsert(demo.id(1)).bin("a").setTo("val1").execute();
txn.upsert(demo.id(2)).bin("b").setTo("val2").execute();
});

Batched writes

For batched writes inside a transaction, attach the transaction to the batch policy.

import com.aerospike.client.sdk.RecordResult;
session.doInTransaction(txn -> {
txn.upsert(demo.ids(1, 2, 3))
.bin("color").setTo("blue")
.execute()
// A batch reports a key's failure in its result: orThrow raises it,
// so that doInTransaction aborts the transaction
.forEach(RecordResult::orThrow);
});

Put, get, and delete in the same transaction

The following example shows multiple commands tied to the same transaction.

session.doInTransaction(txn -> {
txn.upsert(demo.id(1)).bin("a").setTo("val1").execute();
Record rec = txn.query(demo.id(3)).execute().getFirstRecord();
txn.delete(demo.id(3)).withDurableDelete().execute();
});