blob: ce5021f2104f86fffaae06b0467e85e74230912f [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
= Performing Transactions
All queries in Ignite 3 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 link:administrators-guide/data-partitions[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.maxClockSkewMillis`.
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. Ignite will return `TransactionException` on commit attempt.
== Transaction Isolation and Concurrency
All read-write transactions in Ignite 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
Ignite 3 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:
[tabs]
--
tab:Java[]
[source, java]
----
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;
----
tab:.NET[]
[source, csharp]
----
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);
----
tab:C++[]
[source, cpp]
----
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 Timeouts
//Normally, transactions will be executed regardless of how long it takes it to arrive. You can set the transaction timeout in the `TransactionOptions`, in milliseconds. For example:
//[source, java]
//----
//var tx = client.transactions().begin(new TransactionOptions().timeoutMillis(1000));
//int balance = accounts.get(tx, 42).balance;
//tx.commit();
//----
== 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:
[tabs]
--
tab:Java[]
[source,java]
----
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 link:administrators-guide/data-partitions[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:
[source, java]
----
var tx = client.transactions().begin(new TransactionOptions().readOnly(true));
int balance = accounts.get(tx, 42).balance;
tx.commit();
----
NOTE: Read-only transactions read data at a specific time. If new data was written since, old data will still be stored in link:administrators-guide/data-partitions#version-storage[Version Storage] and will be available until low watermark. If low watermark is reached during the transaction, data will be kept available until it is over.