1. Optimistic vs. Pessimistic Locking:
 — Pessimistic: Assumes worst-case scenario
 — Optimistic: Assumes best-case scenario

2. Transaction Isolation Levels in PostgreSQL:
 a. Read Committed (default):
 — Reads: Can see updates from other committed transactions
 — Writes: Waits for conflicting transactions to commit or abort

b. Repeatable Read:
 — Reads: Cannot see writes from transactions that started later
 — Writes: Throws a serializable error if the conflicting transaction commits

c. Serializable:
 — The highest level of isolation
 — Uses Serializable Snapshot Isolation (SSI)
 — Ensures strictest data consistency

3. ACID Compliance:
 — PostgreSQL: Fully ACID-compliant
 — Redis: Not fully ACID-compliant (lacks consistency and durability)
 — MongoDB: ACID-compliant with multi-document transactions
 — Cassandra: Not ACID-compliant in the traditional sense

4. Concurrency Issues:
 — Double booking problem
 — Time-of-check to time-of-use (TOCTOU) bugs

5. Solid Queue:
 — Database-based queuing backend for Active Job
 — Uses row-level locking (FOR UPDATE) to manage concurrent access

6. Locking Options:
 — FOR UPDATE: Locks rows, causing other transactions to wait
 — SKIP LOCKED: Allows transactions to skip locked rows and move to the next available one
 — NOWAIT: Throws an exception immediately if a row is locked

7. Common Concurrency Problems:
 — Lost updates
 — Write skew
 — Race conditions

This report provides an overview of PostgreSQL’s approach to handling concurrent transactions, its isolation levels, and a comparison with other database systems regarding ACID compliance. It also touches on related concepts like queue management and locking strategies to prevent concurrency issues.

postgres optimistic vs pessimistic locking

optimistic VS pessimistic

Pessimistic: The worst case is going on to happen.

Optimistic: The best case is going on to happen.

Race Condition

Access to one row in the database

Transaction

PostgreSQL Concurrency, Locking, and Isolation Levels

Atomicity

PostgreSQL Concurrency, Locking, and Isolation Levels

Isolation

PostgreSQL Concurrency, Locking, and Isolation Levels

Isolation level

PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels

It could happen in PostgreSQL.

PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels
  • When an application process accesses data, the isolation level determines the degree to which that data is locked or isolated from other concurrent processes.
  • Postgres doesn’t have a read uncommitted isolation level, and it also doesn’t allow dirty reads or writes due to its MVCC architecture. (will cover this in the next article)

Read committed (default)

Reads:

  • When multiple transactions are running concurrently, a transaction can see the updates made by other transactions already been committed.
  • These updates can be seen even if the other transactions started and committed while the current transaction was still ongoing.

Writes:

  • If a transaction attempts to update a row that has already been updated by another concurrent transaction (which has not yet been committed), that transaction will pause and wait until the first transaction either commits or aborts.
  • After the first concurrent transaction commits or aborts, the paused transaction can continue with its write.

Repeatable read

Reads:

  • When multiple transactions are running concurrently, a transaction cannot see the writes made by other transactions that started after the current transaction.
  • This applies even if those transactions have already been committed while the current transaction is in progress.

Writes:

  • If a transaction attempts to update a row that has already been updated by another concurrent transaction (which has not yet been committed), it will wait until the first transaction either commits or aborts.
  • However, if the concurrent transaction commits its changes, the paused transaction will throw a serializable error.
  • Essentially, this isolation level ensures that no two transactions can update the same row concurrently.

If we try out the lost update use case mentioned above, the 2nd transaction will throw the following error

ERROR: could not serialize access due to concurrent update

Serializable

This is the highest level of transaction isolation and ensures the strictest level of data consistency. It mimics executing transactions in a serial manner, meaning one after the other instead of concurrently.

Transactions will only be committed if their writings do not affect the result if executed in a different order.

Postgres uses Serializable Snapshot Isolation (SSI) to achieve this isolation (will cover this in detail in next article).

SSI achieves this isolation level in 2 ways:

  1. If a transaction has performed some read queries before a write operation, it performs that read query again while committing and makes sure the result set has not changed. If it has changed, the transaction is aborted.
  2. A writing transaction informs other reading transactions that the latter’s earlier executed search query might have been messed up. In this case, the transaction that commits first would win, and the other one gets aborted.

If we try out the write skew use case mentioned above, the 2nd transaction will throw the following error.

ERROR: could not serialize access due to read/write dependencies among transactions. DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.

ACID

PostgreSQL Concurrency, Locking, and Isolation Levels
PostgreSQL Concurrency, Locking, and Isolation Levels

Redis is not completely ACID-compliant because it does not satisfy consistency nor durability. Redis ensures atomicity because you cannot go to the next transaction till your previous transactions are fully completed. Redis does not guarantee consistency because Redis is at risk of losing writes

In fact, MongoDB is an ACID-compliant database. As of MongoDB 4.0, there is even support for multi-document ACID transactions when required. Version 4.2 even brought distributed multi-document ACID transactions for even more flexibility.

As a non-relational database, Cassandra does not support joins or foreign keys, and consequently does not offer consistency in the ACID sense. For example, when moving money from account A to B the total in the accounts does not change.

https://docs.datastax.com/en/cassandra-oss/2.2/cassandra/dml/dmlTransactionsDiffer.html

Double booking problem

PostgreSQL Concurrency, Locking, and Isolation Levels

time-of-check to time-of-use

In software development, time-of-check to time-of-use (TOCTOU, TOCTTOU or TOC/TOU) is a class of software bugs caused by a race condition involving the checking of the state of a part of a system (such as a security credential) and the use of the results of that check.

What is solid queue?

Solid Queue is database based queuing backend for Active Job. In contrast Sidekiq and Resque are Redis based queuing backends.

The first worker will take the lock on the record using FOR UPDATE. When other workers come to that record and they see that there is a lock FOR UPDATE, they will wait for the lock to be lifted. Yes, these workers will wait until the lock is released.

SKIP LOCKED skips locked rows

sql
START TRANSACTION;
SELECT *
FROM jobs_table FOR UPDATE SKIP LOCKED
LIMIT 1;
-- Process the jobCOMMIT;

Imagine the same scenrio here. A job comes in. Multiple workers compete to claim the job. The database ensures that only one worker gets the lock. However in this case the other workers will move on to the next record. They will not wait. That’s what SKIP LOCKED does.

PostgreSQL Concurrency, Locking, and Isolation Levels

When a transaction attempts to read or lock a row, it may find that another transaction has already locked that row. By default, the new transaction will wait until the previous transaction completes. However, there are two ways to override this behaviour:

  1. NOWAIT: This option allows a transaction to skip the wait and immediately throw an exception when it finds the row is already locked by another transaction.
  2. SKIP LOCKED: This option allows a transaction to skip over any rows locked by other concurrent transactions. As a result, the result set obtained may not contain all the rows requested, but it can complete more quickly without waiting for other transactions to release the locks.

Common problems in concurrent transactions

Visit us at DataDrivenInvestor.com

Subscribe to DDIntel here.

Join our creator ecosystem here.

DDI Official Telegram Channel: https://t.me/+tafUp6ecEys4YjQ1

Follow us on LinkedIn, Twitter, YouTube, and Facebook.