Apache Ignite: Concurrency Modes and Isolation Levels

Pessimistic Concurrency

An example of pessimistic concurrency is transferring funds from one bank account to another. We need to ensure that one bank account is correctly debited and another bank account correctly credited. Locks would be acquired on the two accounts to ensure that updates completed successfully and reflected the new balances of both accounts.
In pessimistic concurrency, applications acquire locks for all the data that need to be read, written or modified at the beginning of the transaction. Apache Ignite also supports a number of isolation levels with pessimistic concurrency, which provide flexibility when reading and writing data:
  • Read Committed
  • Repeatable Read
  • Serializable
In Read Committed mode, the locks are acquired before any changes to the data brought by write operations, such as put() or putAll().The Repeatable Read and Serializable modes are used for situations where locks need to be acquired for both read and write operations. Apache Ignite also has built-in functionality that makes it easier to debug and fix distributed deadlocks.
The following Java code shows an example of a pessimistic transaction with Repeatable Read, as the application needs to perform both read and write operations on a particular bank account.
try (Transaction tx = Ignition.ignite().transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {
    Account acct = cache.get(acctId);

    assert acct != null;

    ...

    // Deposit into account.
    acct.update(amount);

    // Store updated account in cache.
    cache.put(acctId, acct);

    tx.commit();
}
In this example, we have the txStart() and tx.commit() methods to start and commit the transaction, respectively. The txStart() method takes PESSIMISTIC and REPEATABLE READ as parameters. Within the body of the try block, the code performs a cache.get() on the “acctId” key. Further along, some funds are deposited into the account and the cache is updated using cache.put().
The following Java code shows an example of a pessimistic transaction with Read Committed and deadlock handling.
try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.PESSIMISTIC, TransactionIsolation.READ_COMMITTED, TX_TIMEOUT, 0)) {

    // More code here.

    tx.commit();
} catch (CacheException e) {
    if (e.getCause() instanceof TransactionTimeoutException &&
        e.getCause().getCause() instanceof TransactionDeadlockException)

        System.out.println(e.getCause().getCause().getMessage());
}
In this example, the code shows how to use the deadlock detection mechanism in Apache Ignite. This simplifies the debugging of distributed deadlocks that may be caused by application code. To enable this feature, we need to start an Ignite transaction with a non-zero timeout (TX_TIMEOUT > 0) and catch the TransactionDeadlockException that will contain the deadlock details.

Optimistic Concurrency

An example of optimistic concurrency is in Computer Aided Design (CAD), where a designer is working on part of a design and typically checks-out the design from a central repository into a local workstation, then does some updates and checks that design back into the central repository. Since the designer is responsible for a part of the overall design, it is unlikely that there are any update conflicts with other parts of the design.
In contrast to pessimistic concurrency, optimistic concurrency delays lock acquisition. This may be better suited to applications where there is less contention, as in the CAD example described above. Apache Ignite also supports a number of isolation levels with optimistic concurrency, which provide flexibility when reading and writing data:
Recall the discussion from the previous article on the various phases in the two-phase commit protocol. When using optimistic concurrency, during the prepare phase, lock acquisition takes place on the Primary Nodes. When using Serializable mode, if data have changed since requested by a transaction, the transaction will fail at the prepare phase. In this situation, the developer must code the application behavior on whether or not it should restart the transaction. The two other modes, Repeatable Read and Read Committed, never check if data have changed. Whilst these modes may bring performance benefits, there are no data atomicity guarantees and, therefore, these two modes are rarely used in production applications.
The following Java code shows an example of an optimistic transaction with Serializable, as the application needs to perform both read and write operations on a particular bank account.
while (true) {
    try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.OPTIMISTIC, TransactionIsolation.SERIALIZABLE)) {

        Account acct = cache.get(acctId);

        assert acct != null;

        ...

        // Deposit into account.
        acct.update(amount);

        // Store updated account in cache.
        cache.put(acctId, acct);

        tx.commit();

        // Transaction succeeded. Exiting the loop.
        break;
    } catch (TransactionOptimisticException e) {
        // Transaction has failed. Retry.
    }
}
In this example, we have an outer while loop so that if there is a transaction failure, it can be retried. Next, we have the txStart() and tx.commit() methods to start and commit the transaction, respectively. The txStart() method takes OPTIMISTIC and SERIALIZABLE as parameters. Within the body of the try block, the code performs a cache.get() on the “acctId” key. Further along, some funds are deposited into the account and the cache is updated using cache.put(). If the transaction is successful, the code will break from the loop. If the transaction is unsuccessful, an exception occurs and the transaction is retried. For OPTIMISTIC and SERIALIZABLE transactions, keys can be accessed in any order because transaction locks are acquired in parallel with an additional check allowing Apache Ignite to avoid deadlocks.

Kommentare

Beliebte Posts