Deadlock-Free Transactions With Apache Ignite

DZone 's Guide to

Deadlock-Free Transactions With Apache Ignite

Deadlocks can kills services, so see how Apache Ignite avoids it by assigning numbers to transactions in order to compare and utilize them in a fluid manner.

· Big Data Zone ·
Free Resource

In a multi-user environment, concurrent transactions on the same set of cache entries can cause deadlocks — an unfavorable situation that adversely affects the performance of any application. Once a system enters a heavy deadlock state, recovery may require a complete cluster re-start. Keeping this in mind, Apache Ignite came up with ACID-compliant Deadlock-Free Transactions that can prevent deadlocks and enhance application performance. Before looking into this feature in more detail, let’s briefly go through the basics.

What Is a Deadlock?

A deadlock is a situation where a transaction T1 waits indefinitely for a resource R2 that is held by another transaction T2; and T2 waits for a resource R1 that is held by T1. T1 wouldn’t release the lock on R1 until it acquires the lock on R2, and T2 wouldn’t release the lock on R2 until it acquires the lock on R1. 

Deadlocks happen when concurrent transactions try to acquire locks on same objects in different order. A safer approach is to acquire locks in the same order; but this may not be feasible every time.

Preventing Deadlocks in Ignite

When transactions in Ignite are performed with concurrency mode -OPTIMISTIC and isolation level -SERIALIZABLE, locks are acquired during transaction commit with an additional check allowing Ignite to avoid deadlocks. This also prevents cache entries from being locked for extended periods and avoids “freezing” of the whole cluster, thus providing high throughput. Furthermore, during commit, if Ignite detects a read/write conflict or a lock conflict between multiple transactions, only one transaction is allowed to commit. All other conflicting transactions are rolled back and an exception is thrown, as explained in the section below.

How it Works

Ignite assigns version numbers to every transaction and cache entry. These version numbers help decide whether a transaction will be committed or rolled back. Ignite will fail an OPTIMISTIC SERIALIZABLE transaction (T2), with a TransactionOptimisticException exception, if:

  1. There exists an ongoing PESSIMISTIC transaction or OPTIMISTIC transaction with isolation levels- READ-COMMITTED or REPEATABLE-READ (T1), holding a lock on a cache entry requested by T2.

  2. There exists another ongoing OPTIMISTIC SERIALIZABLE transaction (T1) whose version is greater than that of T2, and T1 holds a lock on a cache entry requested by T2.

  3. By the time T2 acquires all required locks, there exists a cache entry with the current version different from the observed version. This is because another transaction T1 has committed and changed the version of the cache entry.


public class DeadlockExample {

    private static final String ENTRY1 = "entry1";
    private static final String ENTRY2 = "entry2";

    public static void main(String[] args) throws IgniteException {
        Ignite ignite = Ignition.start("/myexamples/config/cluster-config.xml");

        // Create cache with given name, if it does not exist.
        final IgniteCache<String, String> cache = ignite.getOrCreateCache("myCache");

        // populate
        int i = 0;
        cache.put(ENTRY1, Integer.toString(i++));
        cache.put(ENTRY2, Integer.toString(i++));

        new Thread(() -> {
            try (Transaction t1 = Ignition.ignite().transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {

                String val1 = cache.get(ENTRY1);
                cache.put(ENTRY1, val1 + "b");

                String val2 = cache.get(ENTRY2);
                cache.put(ENTRY2, val2 + "b");


                System.out.println("t1: " + cache.get(ENTRY1));
                System.out.println("t1: " + cache.get(ENTRY2));
        }, "t1-Thread").start();

        new Thread(() -> {
            try (Transaction t2 = Ignition.ignite().transactions().txStart(OPTIMISTIC, SERIALIZABLE)) {

                String val2 = cache.get(ENTRY2);
                cache.put(ENTRY2, val2 + "c");

                String val1 = cache.get(ENTRY1);
                cache.put(ENTRY1, val1 + "c");


                System.out.println("t2: " + cache.get(ENTRY1));
                System.out.println("t2: " + cache.get(ENTRY2));
        }, "t2-Thread").start();


The output shows that transaction t2 had a lock conflict with t1. Thus, t1 was allowed to commit and t2 was rolled back with an exception.


In a highly concurrent environment, optimistic locking can lead to a high rate of transaction failures. This is still advantageous over pessimistic locking where the possibility of a deadlock occurrence is high. Optimistic-Serializable transactions in Ignite are much faster than pessimistic transactions, and can provide a significant performance improvement to any application. Transactions in Ignite are ACID compliant ensuring guaranteed consistency of data throughout the cluster at all times.

apache ignite ,deadlock detection ,distributed cache ,distributed computing ,in-memory caching ,in-memory computing ,in-memory data grid

Published at DZone with permission of Prachi Garg . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}