An Overview of Databases — Part 8.2: Clocks (MongoDB Causal Consistency)

Saeed Vayghani
6 min readAug 16, 2024

--

Part 1: DBMS Flow
Part 2: Non-Relational DB vs Relational
Part 3: CAP and BASE Theorem

Part 4: How to choose a Database?
Part 5: Different Solutions for Different Problems
Part 6: Concurrency Control
Part 7: Distributed DBMS
>> Part 7.1: Distributed DBMS (Apache Spark, Parquet + Pyspark + Node.js)
>> Part 7.2:
Distributed DBMS (PostgreSQL Partitioning)
Part 8: Clocks
>> Part 8.1: Clocks (MongoDB Replica)
>> Part 8.2: Clocks (MongoDB Causal Consistency)
Part 9: DB Design Mastery
Part 10: Vector DB
Part 11: An interesting case, coming soon!

What we are going to discuss in this post:

  1. MongoDB ClusterTime
  2. MongoDB Sessions with causalConsistency set to true
  3. MongoDB Read Concern
  4. MongoDB Write Concern
  5. MongoDB Read Preference
  6. MongoDB Transactions

1. clusterTime:

Cluster Time is a logical clock used to partially order events across all servers in a MongoDB replica set or sharded cluster. It advances with each write operation and helps ensure that subsequent read operations include all prior writes up to a certain point in time.

  • Purpose: Ensures reads reflect all prior writes up to a specific logical time.
  • Usage: Directly accessed and managed by the application code, typically used in individual operations to enforce causal consistency.
  • Manual Management: Requires the application to manually handle and pass clusterTime to subsequent operations.
  • Scope: Specific to each operation where clusterTime is explicitly used.
const { MongoClient } = require('mongodb');

async function main() {
const uri = "mongodb_uri";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true
await client.connect();

const database = client.db('db_name');
const collection = database.collection('collection_name');
const insertResult = await collection.insertOne({ name: "causal consistency" });
const clusterTime = insertResult.$clusterTime.clusterTime;
const readResult = await collection.find({
$expr: {
$gte: ["$clusterTime", clusterTime]
}
})
await client.close();
}

main()

2. Sessions with causalConsistency set to true

Causal Consistency in Sessions is a feature that automatically tracks and maintains causal relationships between operations within a session. When causalConsistency is set to true, the MongoDB driver ensures that reads within the session reflect the causal order of writes.

  • Purpose: Automatically ensures that all operations in a session reflect a causal order.
  • Usage: Managed transparently by the MongoDB driver, simplifying the development process.
  • Automatic Management: The driver takes care of maintaining and passing necessary clock information within the session.
  • Scope: Applies to all operations (reads and writes) within a session.
const { MongoClient } = require('mongodb');

async function main() {
const uri = "mongodb_uri";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true
await client.connect();

// Start a session with causalConsistency set to true
const session = client.startSession({ causalConsistency: true });
const database = client.db('db_name');
const collection = database.collection('collection_name');

// Start a transaction within the session
session.startTransaction();

// Perform a write operation within the session
await collection.insertOne({ name: "causal consistency" }, { session });

// Perform a read operation within the session
const readResult = await collection.find({}, { session }).toArray();

// Commit transaction
await session.commitTransaction();
session.endSession();

console.log("Read Result:", readResult);

// close db connection
await client.close();
}

main().catch(console.error);

3. Read Concern majority

The majority read concern level is another mechanism in MongoDB to ensure that reads are causally consistent with multiple replica sets. This read concern ensures that the data read from a replica set reflects the most recent acknowledged writes that have been committed to a majority of members at the time of the read.

Features:

  • Consistency Guarantees: Ensures reads are from the most recent majority-committed data, thus maintaining causal consistency with prior acknowledged writes.
  • Automatic Handling: The majority read concern is automatically managed when specified in read operations and does not require manual tracking of logical clocks or sessions.
  • Availability: Available in replica sets and is particularly useful in ensuring higher consistency levels across distributed environments.
const { MongoClient } = require('mongodb');

async function main() {
const uri = "mongodb_uri";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true
await client.connect();

// Get the 'test' database and 'sample' collection
const database = client.db('db_name');
const collection = database.collection('collection_name');

// Insert a document with acknowledge write concern (default)
const insertResult = await collection.insertOne({ name: "causal consistency using majority read concern" });
console.log("Document inserted with _id:", insertResult.insertedId);

// Read data with majority read concern
const readResult = await collection.find().readConcern('majority').toArray();
console.log("Read Results with majority read concern:", readResult);

// close db connection
await client.close();
}

// Execute the main function
main().catch(console.error);

4. Write Concern

Write Concern determines the level of acknowledgment requested from MongoDB for write operations. It ensures that writes are acknowledged with a specified level of durability and replication before proceeding.

Features:

  • Durability: Ensures that writes are acknowledged by a specified number of replica set members.
  • Data Safety: Enhances data safety by waiting for multiple nodes to acknowledge the write.
const { MongoClient } = require('mongodb');

async function main() {
const uri = "mongodb_uri";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true
await client.connect();
const database = client.db('db_name');
const collection = database.collection('collection_name');

// Insert a document with a majority write concern
const insertResult = await collection.insertOne(
{ name: "write concern demonstration" },
{ writeConcern: { w: "majority", j: true } }
);

console.log("Document inserted with write concern majority and journaling:", insertResult.insertedId);
await client.close();
}

main().catch(console.error);

5. Read Preference

Read Preference determines which members of a replica set a client should read from. This helps manage data visibility and freshness by directing reads to primary or secondary nodes based on specific requirements.

Features:

  • Purpose: Directs read operations to specific replica set members.
  • Usage: Helps manage read distribution and data freshness.
  • Benefits: Flexibility in managing read load and data staleness.
const { MongoClient } = require('mongodb');

async function main() {
const uri = "your_mongodb_uri";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
try {
await client.connect();
const database = client.db('test');
const collection = database.collection('sample');

// Read from the primary to ensure the most recent data
const readResultPrimary = await collection.find().readPreference('primary').toArray();
console.log("Read Results using primary read preference:", readResultPrimary);

// Read from a secondary to distribute read load
const readResultSecondary = await collection.find().readPreference('secondary').toArray();
console.log("Read Results using secondary read preference:", readResultSecondary);

} catch (error) {
console.error("An error occurred:", error);
} finally {
await client.close();
}
}

main().catch(console.error);

6. Transactions

Transactions allow you to perform a series of read and write operations within an atomic scope. Transactions ensure that all operations in the transaction complete successfully or none do, maintaining strict consistency.

Features:

  • Atomicity: Ensures all operations within the transaction are either fully completed or fully rolled back.
  • Consistency: Maintains a consistent view of the data, ensuring no partial updates.
  • Isolation: Transactions are isolated from other operations, avoiding race conditions and data anomalies.
const { MongoClient } = require('mongodb');

async function main() {
const uri = "mongodb_uri";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();

const session = client.startSession();
const database = client.db('db_name');
const collection = database.collection('collection_name');

// Start a transaction within the session
session.startTransaction();

try {
// Perform multiple operations within the transaction
await collection.insertOne({ name: "transactional document 1" }, { session });
await collection.insertOne({ name: "transactional document 2" }, { session });

// Commit the transaction
await session.commitTransaction();
console.log("Transaction committed successfully.");

} catch (error) {
// Abort the transaction on error
await session.abortTransaction();
console.error("Transaction aborted due to an error:", error);

} finally {
session.endSession();
}

await client.close();
}
main().catch(console.error);
Summarizing the important information related to the six methods of ensuring consistency in MongoDB.

You can find a simple implementation and some explanation about the above mentioned topic here on a GitHub repository.

--

--

Saeed Vayghani

Software engineer and application architecture. Interested in free and open source software.