Transactions and Concurrency
ACID Properties
Section titled “ACID Properties”ACID is the set of guarantees that a relational database transaction provides. Understanding what Each property actually guarantees — and what it does not — is critical for building correct Concurrent systems.
Atomicity
Section titled “Atomicity”A transaction is an all-or-nothing unit of work. Either all operations in the transaction commit, or None of them do. If the transaction fails at any point (constraint violation, system crash, network Failure), the database rolls back to the state before the transaction began.
Implementation: the database writes changes to a write-ahead log (WAL) before applying them to The data files. On recovery, the WAL is replayed (committed transactions) or undone (uncommitted Transactions).
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
-- If either UPDATE fails (e.g., insufficient funds), both are rolled backCOMMIT;Consistency
Section titled “Consistency”A transaction transforms the database from one valid state to another. All defined constraints (NOT NULL, UNIQUE, CHECK, FOREIGN KEY) must hold at transaction commit. This property is partially the Responsibility of the database (enforcing constraints) and partially the responsibility of the Application (writing correct transaction logic).
Consistency in ACID is not the same as consistency in the CAP theorem. ACID consistency means “the database satisfies all defined constraints.” CAP consistency means “every read returns the most Recent write.” They are different guarantees.
Isolation
Section titled “Isolation”Concurrent transactions should not interfere with each other. The isolation level determines the Degree to which this is enforced. Higher isolation levels provide stronger guarantees but reduce Concurrency and performance.
Durability
Section titled “Durability”Once a transaction commits, its effects are permanent, even in the event of a system crash, power Failure, or hardware fault. The database must guarantee that committed data can be recovered.
Implementation: committed WAL records are flushed to disk (fsync) before the COMMIT returns success To the client. The actual data pages may be flushed to disk later (write-back caching), but the WAL Is the authoritative source for recovery.
-- fsync is the bottleneck for commit latency-- PostgreSQL: synchronous_commit = on (default) means COMMIT waits for WAL fsync-- Setting synchronous_commit = off trades durability for latency (~10x faster commits)-- but may lose the last 100ms of transactions on a crashTransaction States
Section titled “Transaction States”A transaction moves through a well-defined state machine:
stateDiagram-v2
[*] --> Active : BEGIN
Active --> PartiallyCommitted : last statement completes
Active --> Failed : error
PartiallyCommitted --> Committed : WAL flushed to disk
PartiallyCommitted --> Failed : write failure
Failed --> Aborted : rollback complete
Committed --> [*]
Aborted --> [*]| State | Description |
|---|---|
| Active | Transaction is executing statements |
| Partially Committed | All statements executed; waiting for WAL flush |
| Committed | WAL flushed; changes are permanent |
| Failed | An error occurred; changes must be undone |
| Aborted | Rollback completed; database restored to pre-transaction state |
Isolation Levels
Section titled “Isolation Levels”SQL defines four isolation levels, each preventing a different set of concurrency anomalies. The SQL Standard defines three anomalies: dirty reads, non-repeatable reads, and phantom reads.
Anomaly Definitions
Section titled “Anomaly Definitions”Dirty Read: Transaction T1 reads a value written by T2 that has not yet committed. If T2 rolls Back, T1 has read data that never existed.
Non-Repeatable Read: Transaction T1 reads a row, then T2 updates or deletes that row and Commits. When T1 re-reads the row, it sees different data.
Phantom Read: Transaction T1 reads a set of rows matching a condition, then T2 inserts a new row Matching that condition and commits. When T1 re-executes the query, it sees a new (phantom) row.
Isolation Level Comparison
Section titled “Isolation Level Comparison”| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Lock-Based Implementation |
|---|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible | None |
| READ COMMITTED | Prevented | Possible | Possible | Row-level shared locks (duration of read) |
| REPEATABLE READ | Prevented | Prevented | Possible* | Row-level locks held until end of transaction |
| SERIALIZABLE | Prevented | Prevented | Prevented | Range locks or snapshot isolation |
*PostgreSQL”s REPEATABLE READ actually prevents phantom reads through its snapshot-based Implementation, exceeding the SQL standard’s requirement.
READ UNCOMMITTED
Section titled “READ UNCOMMITTED”SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;- No locking for reads
- Can read uncommitted (dirty) data
- Fastest but most dangerous
- Rarely used in practice
- PostgreSQL treats this as READ COMMITTED (it always prevents dirty reads)
Use case: approximate aggregate queries where exact precision is not required (e.g., “roughly how Many orders today?”).
READ COMMITTED
Section titled “READ COMMITTED”SET TRANSACTION ISOLATION LEVEL READ COMMITTED;- Default in PostgreSQL, Oracle, and SQL Server
- Each statement within a transaction sees a fresh snapshot of committed data
- Prevents dirty reads
- Does NOT prevent non-repeatable reads or phantom reads
T1: BEGIN ISOLATION LEVEL READ COMMITTED;T1: SELECT balance FROM accounts WHERE id = 1; -- returns 1000T2: BEGIN;T2: UPDATE accounts SET balance = 900 WHERE id = 1;T2: COMMIT;T1: SELECT balance FROM accounts WHERE id = 1; -- returns 900 (non-repeatable read)T1: COMMIT;REPEATABLE READ
Section titled “REPEATABLE READ”SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;- Default in MySQL (InnoDB)
- The transaction sees a snapshot as of its first read
- Prevents dirty reads and non-repeatable reads
- PostgreSQL prevents phantom reads too; MySQL does not
T1: BEGIN ISOLATION LEVEL REPEATABLE READ;T1: SELECT balance FROM accounts WHERE id = 1; -- returns 1000T2: BEGIN;T2: UPDATE accounts SET balance = 900 WHERE id = 1;T2: COMMIT;T1: SELECT balance FROM accounts WHERE id = 1; -- returns 1000 (repeatable read guaranteed)T1: UPDATE accounts SET balance = balance - 100 WHERE id = 1;-- ERROR: could not serialize access due to concurrent update-- PostgreSQL detects the conflict and aborts T1