Apache Cassandra vs. Apache Ignite: Strong Consistency and Transactions

Apache Ignite: Strong Consistency and ACID TransactionsNoSQL databases, such as Apache Cassandra, are the best-known example of eventually consistent systems. A contract of such systems is simple -- if an application triggered a data change on one machine, then the update will be propagated to all the replicas at some point in time -- in other words, eventually.

Until the change is fully replicated, the system as a whole will stay in an inconsistent state. And who knows where the application will end up if it tries to read the changed value from an out-of-sync replica -- or even worse, update the value concurrently.

NoSQL vendors and users accept this contract and behavior because eventual consistency makes a distributed system highly scalable and performant. That's true. If strong consistency or transactions are required, then examine offerings of the RDBMS world. That's NO longer true!

In the previous article, we figured out that SQL can be efficiently used even if a distributed database is your system of records. Apache Ignite, for instance, allows us not only execute simple SQL operations but to join data stored on several machines easily. What was impossible a decade ago becomes a standard capability of modern distributed databases.  

The same story repeats with consistency and transactions - we can mix-and-match horizontal scalability and high performance of the NoSQL world with capabilities of the RDBMS realm. Let's take Apache Cassandra as a NoSQL database sample and compare it to Apache Ignite that represents modern distributed databases.

Tunable Consistency and Lightweight Transactions

It's not a secret that Cassandra takes care of higher data consistency and transactions. There is a demand for that from its users.

First, if we set write and read consistency levels to ALL will get the highest consistency possible. In this mode, Cassandra will complete a write after it's written to the commit log and memtable on all replica nodes in the cluster. A read, respectively, will return a value only after all the replicas agree on it. It's a handy capability that brings down performance in favor of consistency and can be enabled if you need it. 

Second, this write-read `ALL` mode doesn't solve a problem of concurrent updates. What if we want to update a user account making sure nobody intervents while the update happens? Transactions usually address such tasks, and Cassandra users can leverage from so-called lightweight transactions (LWT).

The lightweight transactions are explicitly designed for the situations to prevent concurrent updates of a single row. For instance, if two different applications are attempting to update a user account, then the LWT ensures that only one application succeeds while the other fails. Assuming that the first application will initiate a transaction earlier, we can safely and atomically set Bob Smith's age to 35 as follows:

UPDATE user_account
SET    user_age=35
IF     user_id=’Bob Smith’; 

But what about more complex operations such as a money transfer from one bank account to another? Unfortunately, that's out of the scope of both Cassandra and its LWT because the latter is restricted to a single partition while the banks account can be stored on different cluster nodes.

Strong Consistency and ACID Transactions

While the money transfer is a big deal for Cassandra, it's a typical operation you can do in Apache Ignite.

First, to achieve the strongest consistency in Ignite we will set FULL_SYNC synchronization and transactional modes for caches (aka. tables). We can even request Ignite to fsync every update to a write-ahead-log file to survive power failures of the whole cluster.

Second, by using Ignite transactional APIs, we can do the money transfers between two accounts that might be stored on different nodes:


try (Transaction tx = Ignition.ignite().transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {
    Account acctA = accounts.get(acctAid);
    
    Account acctB = accounts.get(acctBid);
    
    // Withdraw from accountB.
    acctB.update(amount);
    
    // Deposit into accountA.
    acctA.update(amount);

    // Store updated accounts in the cluster.
    accounts.put(acctAid, acctA);
    accounts.put(acctBid, acctB);

    tx.commit();
}

That's it. Ignite ACID transactions are based on an advanced version of two-phase commit protocol (2PC) that guarantees data consistency even during failures. Refer to this blogging series to learn more about Ignite's transactional subsystem implementation details. 

Upshot

Apache Ignite proves that distributed ACID transactions and strong consistency are real and widely adopted by modern databases along with horizontal scalability and high availability. It's all about configuration and your needs. Just check out how many financial institutions trust Ignite with the their mission-critical applications --confirming that this ASF project is not bluffing.