Performing Transactions
All queries in GridGain 9 are transactional. You can provide an explicit transaction as a first argument of any Table and SQL API call. If you do not provide an explicit transaction, an implicit one will be created for every call.
Transaction Lifecycle
When the transaction is created, the node that the transaction was started from is chosen as transaction coordinator. The coordinator finds the required partitions and sends the read or write requests to the nodes holding primary partitions. For correct transaction operation, all nodes in cluster must have similar time, that can be different by no more than schemaSync.maxClockSkew
.
If the key is not locked by a different transaction, the node gets the locks on the involved keys, and attempts to apply the changes in the transaction. When the operation finishes, the lock is removed. This way, several transactions can work on the same partition, while changing separate keys. Additionally, some operations may perform short-term locks on the keys in advance, to ensure operations proceed correctly.
If the node with primary replica of the partition involved in the transaction fail, the transaction is eventually automatically rolled back. GridGain will return TransactionException
on commit attempt.
Transaction Isolation and Concurrency
All read-write transactions in GridGain acquire locks during the first read or write access, and hold the lock until the transaction is committed or rolled back. All read-write transactions are SERIALIZABLE
, so as long as the lock persists, no other transaction can make changes to locked data, however data can still be read by Read-Only Transactions.
Deadlock Prevention
GridGain 9 uses the WAIT_DIE
deadlock prevention algorithm. When a newer transaction requests data that is already locked by a different transaction, it is cancelled and the transaction operation is retried with the same timestamp. If the transaction is older, it is not cancelled and is allowed to wait for the lock to be freed.
Executing Transactions
Here is how you can provide a transaction explicitly:
KeyValueView<Long, Account> accounts =
table.keyValueView(Mapper.of(Long.class), Mapper.of(Account.class));
accounts.put(null, 42, new Account(16_000));
var tx = client.transactions().begin();
Account account = accounts.get(tx, 42);
account.balance += 500;
accounts.put(tx, 42, account);
assert accounts.get(tx, 42).balance == 16_500;
tx.rollback();
assert accounts.get(tx, 42).balance == 16_000;
var accounts = table.GetKeyValueView<long, Account>();
await accounts.PutAsync(transaction: null, 42, new Account(16_000));
await using ITransaction tx = await client.Transactions.BeginAsync();
(Account account, bool hasValue) = await accounts.GetAsync(tx, 42);
account = account with { Balance = account.Balance + 500 };
await accounts.PutAsync(tx, 42, account);
Debug.Assert((await accounts.GetAsync(tx, 42)).Value.Balance == 16_500);
await tx.RollbackAsync();
Debug.Assert((await accounts.GetAsync(null, 42)).Value.Balance == 16_000);
public record Account(decimal Balance);
auto accounts = table.get_key_value_view<account, account>();
account init_value(42, 16'000);
accounts.put(nullptr, {42}, init_value);
auto tx = client.get_transactions().begin();
std::optional<account> res_account = accounts.get(&tx, {42});
res_account->balance += 500;
accounts.put(&tx, {42}, res_account);
assert(accounts.get(&tx, {42})->balance == 16'500);
tx.rollback();
assert(accounts.get(&tx, {42})->balance == 16'000);
Transaction Management
You can also manage transactions by using the runInTransaction
class. When using it, the following will be done automatically:
-
The transaction is started and substituted to the closure.
-
The transaction is committed if no exceptions were thrown during the closure.
-
The transaction will be retried in case of recoverable error. Closure must be purely functional - not causing side effects.
Here is the example of a transaction that transfers money from one account to another, and handles a possible overdraft:
igniteTransactions.runInTransaction(tx -> {
CompletableFuture<Tuple> fut1 = view.getAsync(tx, Tuple.create().set("accountId", 1));
CompletableFuture<Tuple> fut2 = view.getAsync(tx, Tuple.create().set("accountId", 2)); // Read second balance concurrently
if (fut1.join().doubleValue("balance") - amount < 0) {
tx.rollback();
return;
}
view.upsert(tx, Tuple.create().set("accountId", 1).set("balance", fut1.join().doubleValue("balance") - amount));
view.upsert(tx, Tuple.create().set("accountId", 2).set("balance", fut2.join().doubleValue("balance") + amount);
});
Read-Only Transactions
When starting a transaction, you can configure the transaction as a read-only transaction. In these transactions, no data modification can be performed, but they also do not secure locks and can be performed on non-primary partitions, further improving their performance. Read-only transactions always check the data for the moment they were started, even if new data was written to the database.
Here is how you can make a read-only transaction:
var tx = client.transactions().begin(new TransactionOptions().readOnly(true));
int balance = accounts.get(tx, 42).balance;
tx.commit();
© 2024 GridGain Systems, Inc. All Rights Reserved. Privacy Policy | Legal Notices. GridGain® is a registered trademark of GridGain Systems, Inc.
Apache, Apache Ignite, the Apache feather and the Apache Ignite logo are either registered trademarks or trademarks of The Apache Software Foundation.