Open In App

Strict consistency or Linearizability in System Design

Last Updated : 21 Jul, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

Strong consistency (strict consistency) provides the strongest level of consistency. It ensures that all operations appear to execute atomically and in a globally agreed-upon order. Reads always return the most recent write. Strict consistency, also known as linearizability, is a consistency model in distributed systems that provides the illusion of executing operations atomically and sequentially. In other words, it ensures that the system’s behavior appears as if all operations happened in a linear order, even though they may have been executed concurrently.

Strict Consistency

Under linearizability: Each operation has an associated point in time called its linearization point. The linearization point of an operation is a moment in time when the operation appears to take effect atomically, without any interference from other concurrent operations. This model guarantees that the system behaves as if all operations occurred in a sequential and consistent manner.

Different Approaches to Achieving Strict Consistency aka Linearizability:

Achieving strict consistency or linearizability in distributed systems can be challenging. Here are a few common approaches:

(a) Distributed Locking:

One approach is to use distributed locks, such as ZooKeeper or Consul, to enforce mutual exclusion on shared resources. By acquiring locks before performing operations, you can ensure that operations appear to execute atomically and sequentially, maintaining strict consistency.

(b) Two-Phase Commit (2PC):

The two-phase commit protocol is a coordination mechanism used to ensure atomicity and consistency across multiple distributed nodes. It involves a coordinator and multiple participants. The coordinator proposes a transaction to all participants, and if all participants agree, the coordinator commits the transaction. If any participant disagrees or fails, the coordinator aborts the transaction. 2PC can be used to enforce strict consistency in distributed systems.

(c) Distributed Data Store with Consensus:

Using a distributed data store that provides consensus algorithms, such as Apache Cassandra or Google Spanner, can ensure strict consistency. These systems replicate data across multiple nodes and use consensus algorithms like Paxos or Raft to ensure that all operations are linearizable.

How locks can be used to ensure linearizability within a single process

Example: Implementing strict consistency requires a complex distributed system design. Below is a simplified code example in Node.js to demonstrate how locks can be used to ensure linearizability within a single process:

Javascript




// This line imports the Lock class
// from the 'distributed-lock'
// library.
const { Lock } = require('distributed-lock');
 
// This is a class declaration named BankAccount. It has a
// constructor that takes an initial balance as a parameter
// and initializes the balance property with the provided
// value. It also creates a new instance of the Lock class
// and assigns it to the lock property.
class BankAccount {
    constructor(initialBalance)
    {
        this.balance = initialBalance;
        this.lock = new Lock();
    }
 
    async deposit(amount)
    {
        await this.lock.acquire();
        try {
            const currentBalance = this.balance;
            // Simulating some processing time
            await sleep(200);
            this.balance = currentBalance + amount;
        }
        finally {
            this.lock.release();
        }
    }
 
    async withdraw(amount)
    {
        await this.lock.acquire();
        try {
            const currentBalance = this.balance;
            // Simulating some processing time
            await sleep(200);
            if (currentBalance >= amount) {
                this.balance = currentBalance - amount;
                return true;
            }
            return false;
        }
        finally {
            this.lock.release();
        }
    }
}
 
// This is an asynchronous function testBankAccount.
// It creates a new instance of BankAccount with an
// initial balance of 100. It then uses Promise.all
// to concurrently execute multiple operations: a
// deposit of 50, a withdrawal of 70, and another
// deposit of 30. Finally, it logs the final balance
// of the account object.
async function
testBankAccount()
{
    const account = new BankAccount(100);
    await Promise.all([
        account.deposit(50),
        account.withdraw(70),
        account.deposit(30),
    ]);
 
    console.log('Final balance:',
                account.balance); // Expected output:


Explanation:

  • This code demonstrates the significance of distributed locking for achieving strict consistency in a concurrent programming scenario. The code implements a BankAccount class that allows concurrent deposits and withdrawals while ensuring the integrity of the account balance using a distributed lock.
  • Distributed locking is essential in scenarios where multiple threads or processes access shared resources concurrently. In this case, the shared resource is the balance property of the BankAccount object, and the Lock class from the ‘distributed-lock’ library is used to protect it.
  • The BankAccount class constructor initializes the balance property with an initial value and creates a new instance of the Lock class, which is assigned to the lock property. This lock will be used to synchronize access to the balance property.
  • The deposit and withdraw methods are defined as asynchronous functions. They both acquire the lock using await this.lock.acquire() at the beginning to ensure exclusive access to the critical section of code that manipulates the balance.
  • Inside the critical section, the current balance is first stored in the currentBalance variable. This step is crucial to maintain consistency because another concurrent operation may modify the balance before the current operation completes.
  • To simulate some processing time, the code includes an await sleep(200) statement, which delays the execution for 200 milliseconds. This delay is not necessary for the lock mechanism itself but is added here to illustrate potential concurrency issues.
  • In the deposit method, the amount is added to the currentBalance, and the updated balance is stored back in the this.balance property.
  • In the withdraw method, the code checks if the currentBalance is sufficient to cover the withdrawal amount. If so, the amount is subtracted from the currentBalance, and the updated balance is stored back in the this.balance property. If the withdrawal amount exceeds the balance, the method returns false.
  • Finally, both the deposit and withdraw methods release the lock using this.lock.release() in the finally block. Releasing the lock ensures that other concurrent operations can acquire it and proceed.
  • The testBankAccount function demonstrates the concurrent usage of the BankAccount class. It creates an instance of BankAccount with an initial balance of 100. Then, using Promise.all, it concurrently executes three operations: a deposit of 50, a withdrawal of 70, and another deposit of 30.
  • By acquiring and releasing the lock appropriately, the code ensures that the deposit and withdrawal operations are executed atomically, without interference from other concurrent operations. This guarantees strict consistency in the account balance.
  • After all the operations complete, the final balance of the account object is logged to the console using console.log(‘Final balance:’, account.balance).


Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads