Database Transactions: ACID, Isolation Levels & Deadlocks
Database transactions are fundamental to ensuring data consistency and integrity in modern applications. They enable safe, reliable operations even when multiple users access the database simultaneously.
In the world of databases, we face critical challenges: How do we ensure that money isn’t lost during transfers? How do we prevent two users from modifying the same data at once and overwriting each other’s changes? How do we guarantee that system crashes don’t leave the database in an inconsistent state?
The answer lies in transactions—the core mechanism for safe database operations. Transactions follow the ACID properties (Atomicity, Consistency, Isolation, Durability), which ensure that database operations either complete fully or don’t happen at all.
When multiple users access the database concurrently, new challenges emerge: Isolation levels define how much transactions can affect each other, ranging from simple read problems to phantom rows. Deadlocks can occur when transactions wait on each other and become mutually blocked. Optimistic concurrency offers a modern alternative to traditional pessimistic locking by detecting and resolving conflicts at commit time.
These concepts aren’t merely theoretical—they’re essential for banking systems, e-commerce platforms, social media applications, and any software that works reliably with persistent data.
What Are Transactions?
Definition and Basics
A transaction is a logical unit of work consisting of one or more database operations. Transactions must be treated as an atomic unit—either all operations succeed or none do.
Transaction Properties
-- Transaction as a logical unit
BEGIN TRANSACTION;
-- Operation 1: Debit account
UPDATE Accounts SET balance = balance - 100 WHERE account_id = 1;
-- Operation 2: Credit account
UPDATE Accounts SET balance = balance + 100 WHERE account_id = 2;
-- Either both operations succeed or neither does
COMMIT;
-- or on error: ROLLBACK;
ACID Properties
The ACID properties form the foundation of reliable transaction systems. They guarantee that database operations remain consistent and dependable even in the face of errors, system crashes, or concurrent access by multiple users. Each property addresses specific challenges: Atomicity prevents partial operations, Consistency preserves business rules, Isolation protects against mutual interference, and Durability ensures permanent storage.
Atomicity
Atomicity ensures a transaction executes completely or not at all. This is critical for operations involving multiple steps, such as bank transfers, where money must be debited from one account and credited to another simultaneously. Without atomicity, system crashes could leave partial operations behind, resulting in inconsistent data.
-- Example of atomicity
CREATE TABLE Transactions (
trans_id INT PRIMARY KEY AUTO_INCREMENT,
from_account INT,
to_account INT,
amount DECIMAL(10,2),
status VARCHAR(20),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Atomic transfer
DELIMITER //
CREATE PROCEDURE transfer(
IN from_account_id INT,
IN to_account_id INT,
IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- Check if balance is sufficient
DECLARE current_balance DECIMAL(10,2);
SELECT balance INTO current_balance FROM Accounts WHERE account_id = from_account_id;
IF current_balance < amount THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient balance';
END IF;
-- Debit account
UPDATE Accounts SET balance = balance - amount WHERE account_id = from_account_id;
-- Credit account
UPDATE Accounts SET balance = balance + amount WHERE account_id = to_account_id;
-- Log transaction
INSERT INTO Transactions (from_account, to_account, amount, status)
VALUES (from_account_id, to_account_id, amount, 'SUCCESS');
COMMIT;
END //
DELIMITER ;
-- Call the procedure
CALL transfer(1, 2, 100.00);
What this example demonstrates: The stored procedure ensures perfect atomicity through DECLARE EXIT HANDLER FOR SQLEXCEPTION, which automatically executes a ROLLBACK on any error. The entire transfer (balance check, debit, credit, logging) is treated as an indivisible unit.
Key aspects of this example:
- Error Handling: The handler catches all SQL errors and triggers automatic rollback
- Balance Validation: Before execution, we verify sufficient funds exist
- Completeness: All four steps must succeed, otherwise none are applied
- Logging: The transaction is only recorded on success, ensuring audit trail consistency
Consistency
Consistency ensures the database remains in a valid state after a transaction completes. This property preserves business rules and data integrity by ensuring all defined constraints, triggers, and relationships are enforced. For a bank transfer, this means the total balance across all accounts stays constant and no account can go negative.
-- Example of consistency rules
CREATE TABLE Accounts (
account_id INT PRIMARY KEY,
owner VARCHAR(100),
balance DECIMAL(10,2) NOT NULL,
CHECK (balance >= 0) -- Balance cannot be negative
);
CREATE TABLE TransferRules (
rule_id INT PRIMARY KEY,
max_amount_per_day DECIMAL(10,2),
max_transfers_per_day INT
);
-- Consistency-preserving transaction
DELIMITER //
CREATE PROCEDURE consistent_transfer(
IN from_account_id INT,
IN to_account_id INT,
IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- Business rule: Check maximum amount
DECLARE max_amount DECIMAL(10,2);
SELECT max_amount_per_day INTO max_amount
FROM TransferRules WHERE rule_id = 1;
IF amount > max_amount THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Amount exceeds maximum';
END IF;
-- Balance check (enforced by CHECK constraint)
DECLARE current_balance DECIMAL(10,2);
SELECT balance INTO current_balance FROM Accounts WHERE account_id = from_account_id;
IF current_balance < amount THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
END IF;
-- Execute operations
UPDATE Accounts SET balance = balance - amount WHERE account_id = from_account_id;
UPDATE Accounts SET balance = balance + amount WHERE account_id = to_account_id;
COMMIT;
END //
DELIMITER ;
What this example demonstrates: Consistency is safeguarded through multiple mechanisms: the CHECK (balance >= 0) constraint prevents negative balances, the stored procedure validates business rules before execution, and any violation triggers automatic transaction rollback.
Key aspects of this example:
- Database Constraints: The CHECK constraint enforces business rules at the database level
- Business Logic Validation: The stored procedure implements additional business rules
- Automatic Rollback: Constraint violations cause the transaction to abort automatically
- Data Integrity: Multiple layers of consistency protection (constraint plus application logic)
Isolation
Isolation ensures that concurrently executing transactions do not interfere with each other. This prevents classic concurrency problems such as Dirty Reads (reading uncommitted data), Non-Repeatable Reads (different results when reading the same data twice), and Phantom Reads (new rows appearing between reads). Isolation is controlled through various isolation levels, which balance consistency against performance.
-- Example of Isolation
-- Session 1:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- Session 2 (parallel):
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- Session 1 reads data
SELECT * FROM Konten WHERE konto_id = 1;
-- Session 2 modifies data
UPDATE Konten SET saldo = 1500 WHERE konto_id = 1;
COMMIT;
-- Session 1 reads again (depends on isolation level)
SELECT * FROM Konten WHERE konto_id = 1;
Durability
Durability ensures that changes made by a transaction are permanently stored.
-- Example of Durability
-- After COMMIT, changes are permanent
START TRANSACTION;
UPDATE Konten SET saldo = 2000 WHERE konto_id = 1;
COMMIT; -- Changes are now permanent
-- Changes persist even in the event of system failure
-- (through Write-Ahead Logging and other mechanisms)
Isolation Levels
Isolation levels define how much concurrently executing transactions are allowed to interfere with each other. They provide a crucial tradeoff between data consistency and system performance: higher isolation means greater safety but also more overhead and potentially slower operations. Choosing the right isolation level depends on your specific application requirements and consistency needs.
READ UNCOMMITTED
The lowest isolation level, allowing Dirty Reads. This level is rarely used because it can lead to inconsistent data, but it maximizes performance through minimal locking.
-- READ UNCOMMITTED example
-- Session 1:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE Konten SET saldo = 500 WHERE konto_id = 1;
-- Not yet committed!
-- Session 2:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM Konten WHERE konto_id = 1;
-- Reads the uncommitted value (500) - Dirty Read!
-- Session 1:
ROLLBACK; -- Change is rolled back
-- Session 2 has read invalid data
What this example demonstrates: READ UNCOMMITTED allows reading data that has not yet been committed. Session 2 reads a value (500) that Session 1 later rolls back, resulting in inconsistent data.
Key points in this example:
- Dirty Read Problem: Session 2 reads uncommitted data that becomes invalid later
- Performance Advantage: No locking required, maximum read speed
- Data Inconsistency: Risk of reading inconsistent results
- Use Case: Only suitable for systems where absolute data consistency is not critical
READ COMMITTED
Prevents Dirty Reads but allows Non-Repeatable Reads.
-- READ COMMITTED example
-- Session 1:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT saldo FROM Konten WHERE konto_id = 1; -- Reads 1000
-- Session 2:
START TRANSACTION;
UPDATE Konten SET saldo = 1500 WHERE konto_id = 1;
COMMIT;
-- Session 1 reads again:
SELECT saldo FROM Konten WHERE konto_id = 1; -- Now reads 1500
-- Non-Repeatable Read!
REPEATABLE READ
Prevents Dirty Reads and Non-Repeatable Reads but allows Phantom Reads.
-- REPEATABLE READ example
-- Session 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM Konten WHERE inhaber LIKE 'A%'; -- Reads 3 accounts
-- Session 2:
START TRANSACTION;
INSERT INTO Konten VALUES (4, 'Anna Schmidt', 2000);
COMMIT;
-- Session 1 reads again:
SELECT * FROM Konten WHERE inhaber LIKE 'A%'; -- Still 3 accounts
-- New row is not visible (no Phantom Read in MySQL)
SERIALIZABLE
The highest isolation level, preventing all anomalies.
-- SERIALIZABLE example
-- Session 1:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT AVG(saldo) FROM Konten; -- Calculates average
-- Session 2:
START TRANSACTION;
INSERT INTO Konten VALUES (5, 'Bernd Mueller', 3000);
-- Waits until Session 1 completes!
-- Session 1:
COMMIT;
-- Session 2 can now proceed
COMMIT;
Concurrency Control
Pessimistic Concurrency Control
Locks resources proactively to prevent conflicts.
-- Pessimistic Locking example
-- Explicit Locks
START TRANSACTION;
-- Lock row
SELECT * FROM Konten WHERE konto_id = 1 FOR UPDATE;
-- Other transactions must wait
-- Session 2:
SELECT * FROM Konten WHERE konto_id = 1 FOR UPDATE;
-- Waits until Session 1 commits/rolls back
-- Perform operations
UPDATE Konten SET saldo = saldo - 100 WHERE konto_id = 1;
COMMIT; -- Lock is released
Optimistic Concurrency Control
Assumes that conflicts are rare and resolves them as needed. This approach is particularly effective in systems with many read operations but few writes. Instead of proactively locking resources, conflicts are detected during validation and the transaction is retried if necessary. This reduces locking overhead and improves scalability.
-- Optimistic Concurrency with Version Column
CREATE TABLE Produkte (
produkt_id INT PRIMARY KEY,
name VARCHAR(100),
preis DECIMAL(10,2),
bestand INT,
version INT DEFAULT 0
);
-- Update with version check
DELIMITER //
CREATE PROCEDURE update_produkt_optimistic(
IN produkt_id INT,
IN neuer_preis DECIMAL(10,2),
IN erwartete_version INT
)
BEGIN
DECLARE affected_rows INT;
START TRANSACTION;
UPDATE Produkte
SET preis = neuer_preis, version = version + 1
WHERE produkt_id = produkt_id AND version = erwartete_version;
SET affected_rows = ROW_COUNT();
IF affected_rows = 0 THEN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Concurrency conflict - product was modified';
ELSE
COMMIT;
END IF;
END //
DELIMITER ;
-- Application usage
-- First read
SELECT produkt_id, name, preis, version FROM Produkte WHERE produkt_id = 1;
-- Update with expected version
CALL update_produkt_optimistic(1, 29.99, 5);
What this example demonstrates: Optimistic Concurrency Control uses a version column to detect conflicts. The UPDATE statement checks whether the expected version is still current and increments the version on successful update.
Key points in this example:
- Version Column: Each change increments the version, enabling a change history
- Conflict Detection: The WHERE clause checks the version before the update
- No Locking: No explicit locks required, better performance for read operations
- Retry Logic: On conflict, the application must retry the transaction
Deadlocks
Deadlock Detection and Prevention
Deadlocks occur when transactions wait for each other and become mutually blocked. This is a classic concurrency problem: two or more transactions each hold a lock on a resource that another transaction needs, while simultaneously waiting for a resource locked by that other transaction. Deadlocks cause the system to hang and must be detected and resolved automatically, typically by aborting one of the involved transactions.
-- Deadlock example
-- Session 1:
START TRANSACTION;
UPDATE Konten SET saldo = saldo - 100 WHERE konto_id = 1;
-- Waits for konto_id = 2
UPDATE Konten SET saldo = saldo + 100 WHERE konto_id = 2;
-- Session 2 (parallel):
START TRANSACTION;
UPDATE Konten SET saldo = saldo - 50 WHERE konto_id = 2;
-- Waits for konto_id = 1
UPDATE Konten SET saldo = saldo + 50 WHERE konto_id = 1;
-- DEADLOCK! Both are waiting for each other
What this example shows: A textbook deadlock scenario: Session 1 locks account 1 and then tries to access account 2, while Session 2 locks account 2 and then tries to access account 1. The two transactions end up blocking each other indefinitely.
Key points in this example:
- Circular Wait: Both transactions are waiting for resources held by the other
- Resource Holding: Each transaction holds one lock while waiting to acquire another
- Deadlock Detection: The database must recognize the deadlock and abort one transaction
- Prevention Strategy: A consistent lock ordering could have prevented this deadlock entirely
Deadlock Avoidance Strategies
-- 1. Consistent lock ordering
DELIMITER //
CREATE PROCEDURE sichere_ueberweisung(
IN von_konto_id INT,
IN zu_konto_id INT,
IN betrag DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
-- Always lock in the same order
IF von_konto_id < zu_konto_id THEN
SET @lock1 = von_konto_id;
SET @lock2 = zu_konto_id;
ELSE
SET @lock1 = zu_konto_id;
SET @lock2 = von_konto_id;
END IF;
START TRANSACTION;
-- Acquire locks in consistent order
SELECT * FROM Konten WHERE konto_id = @lock1 FOR UPDATE;
SELECT * FROM Konten WHERE konto_id = @lock2 FOR UPDATE;
-- Perform operations
IF von_konto_id < zu_konto_id THEN
UPDATE Konten SET saldo = saldo - betrag WHERE konto_id = von_konto_id;
UPDATE Konten SET saldo = saldo + betrag WHERE konto_id = zu_konto_id;
ELSE
UPDATE Konten SET saldo = saldo + betrag WHERE konto_id = zu_konto_id;
UPDATE Konten SET saldo = saldo - betrag WHERE konto_id = von_konto_id;
END IF;
COMMIT;
END //
DELIMITER ;
-- 2. Timeout-based retry logic
DELIMITER //
CREATE PROCEDURE ueberweisung_mit_retry(
IN von_konto_id INT,
IN zu_konto_id INT,
IN betrag DECIMAL(10,2)
)
BEGIN
DECLARE retry_count INT DEFAULT 0;
DECLARE max_retries INT DEFAULT 3;
DECLARE deadlock_detected BOOLEAN DEFAULT FALSE;
retry_loop: WHILE retry_count < max_retries DO
BEGIN
DECLARE EXIT HANDLER FOR 1213 -- Deadlock error code
BEGIN
SET deadlock_detected = TRUE;
SET retry_count = retry_count + 1;
IF retry_count < max_retries THEN
-- Brief backoff before retry
DO SLEEP(0.1 * retry_count);
END IF;
END;
-- Execute the transaction
CALL sichere_ueberweisung(von_konto_id, zu_konto_id, betrag);
-- Success - exit loop
LEAVE retry_loop;
END;
IF deadlock_detected AND retry_count >= max_retries THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Max retries exceeded';
END IF;
SET deadlock_detected = FALSE;
END WHILE;
END //
DELIMITER ;
Deadlock Monitoring
-- Retrieve deadlock information (MySQL)
SHOW ENGINE INNODB STATUS;
-- Monitor transactions involved in deadlocks
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
-- Display lock status
SELECT
object_name,
lock_type,
lock_mode,
lock_status,
engine_transaction_id
FROM performance_schema.data_locks
WHERE object_name = 'Konten';
Transaction Management Across Different Databases
MySQL/MariaDB
-- MySQL-specific features
-- Control autocommit behavior
SET autocommit = 0; -- Manual transaction control
SET autocommit = 1; -- Automatic commit (default)
-- Savepoints for partial rollbacks
START TRANSACTION;
UPDATE Konten SET saldo = saldo - 100 WHERE konto_id = 1;
SAVEPOINT sp1;
UPDATE Konten SET saldo = saldo - 50 WHERE konto_id = 2;
SAVEPOINT sp2;
-- Roll back to a specific savepoint
ROLLBACK TO sp1;
COMMIT; -- Only the first change persists
-- XA transactions for distributed systems
XA START 'xid1';
UPDATE Konten SET saldo = saldo - 100 WHERE konto_id = 1;
XA END 'xid1';
XA PREPARE 'xid1';
XA COMMIT 'xid1';
PostgreSQL
-- PostgreSQL-specific features
-- Transaction isolation levels
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Advisory locks (application-level locking)
SELECT pg_advisory_lock(12345); -- Acquire lock
SELECT pg_advisory_unlock(12345); -- Release lock
-- Transaction snapshots
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM Konten WHERE konto_id = 1;
-- In another session:
UPDATE Konten SET saldo = 2000 WHERE konto_id = 1;
COMMIT;
-- Original session still sees the old value
SELECT * FROM Konten WHERE konto_id = 1;
Oracle
-- Oracle-specific features
-- Read-only transactions
SET TRANSACTION READ ONLY;
SELECT * FROM Konten; -- Guarantees a consistent view
-- Autonomous transactions
DELIMITER //
CREATE PROCEDURE log_transaktion(
IN transaktion_id INT,
IN beschreibung VARCHAR(200)
)
AS
BEGIN
-- Autonomous transaction
PRAGMA AUTONOMOUS_TRANSACTION;
INSERT INTO Transaktionslog (trans_id, beschreibung, zeitpunkt)
VALUES (transaktion_id, beschreibung, SYSTIMESTAMP);
COMMIT; -- Commit applies only to the autonomous transaction
END;
//
-- Savepoints
SAVEPOINT sp1;
-- Operations
ROLLBACK TO sp1;
Best Practices for Transaction Management
1. Keep transactions short
-- Bad: Long-running transaction
START TRANSACTION;
SELECT * FROM grosse_tabelle; -- Slow query
-- ... many other operations ...
UPDATE kleine_tabelle SET wert = 1;
COMMIT;
-- Good: Short, focused transaction
SELECT * FROM grosse_tabelle; -- Outside the transaction
START TRANSACTION;
UPDATE kleine_tabelle SET wert = 1;
COMMIT;
2. Choose the right isolation level
-- For most application scenarios
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- For analytical queries
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- For critical financial operations
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
3. Implementing error handling
-- Robust error handling
DELIMITER //
CREATE PROCEDURE robuste_transaktion()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1 @sqlstate = RETURNED_SQLSTATE,
@errno = MYSQL_ERRNO,
@text = MESSAGE_TEXT;
ROLLBACK;
-- Logging
INSERT INTO error_log (error_time, error_code, error_message)
VALUES (NOW(), @errno, @text);
-- Re-raise error
RESIGNAL;
END;
START TRANSACTION;
-- Transaction logic
INSERT INTO tabelle1 (wert) VALUES (1);
UPDATE tabelle2 SET wert = 2 WHERE id = 1;
COMMIT;
END //
DELIMITER ;
4. Using connection pooling
// Java example with connection pool
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class TransactionService {
private DataSource dataSource;
public void executeInTransaction(TransactionCallback callback) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
callback.execute(conn);
conn.commit();
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
throw new RuntimeException("Transaction failed", e);
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
@FunctionalInterface
public interface TransactionCallback {
void execute(Connection conn) throws SQLException;
}
}
Exam-relevant concepts
Key ACID properties
| Property | Description | Implementation |
|---|---|---|
| Atomicity | All or nothing | Rollback, Write-Ahead Logging |
| Consistency | Consistent state | Constraints, Triggers |
| Isolation | No interference | Locks, Isolation Levels |
| Durability | Permanent storage | Redo Logs, Checkpoints |
Isolation levels comparison
| Level | Dirty Reads | Non-Repeatable Reads | Phantom Reads | Performance |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | Highest |
| READ COMMITTED | ❌ | ✅ | ✅ | High |
| REPEATABLE READ | ❌ | ❌ | ✅ (MySQL: ❌) | Medium |
| SERIALIZABLE | ❌ | ❌ | ❌ | Lowest |
Common exam questions
- Explain ACID properties
- Compare isolation levels
- Implement deadlock prevention
- Choose appropriate transaction strategies
- Analyze concurrency issues
Summary
Transaction management is fundamental to building reliable database applications:
- ACID properties guarantee data integrity
- Isolation levels control concurrent behavior
- Deadlock prevention ensures system stability
- Optimistic vs pessimistic concurrency control
- Best practices optimize performance and reliability
Effective transaction design requires understanding both your application’s requirements and the underlying database mechanisms.
Recommended reading: Databases
Keine Bücher für Kategorie "datenbanken" gefunden.

