Careeroza
Isolation Levels
Concurrent Transactions and Anomalies
Thousands of users buy at the same time. Isolation controls how much one transaction sees of others that are still running.
| Anomaly | What happens |
|---|---|
| Dirty read | you read a value another transaction wrote but has not committed (and may roll back) |
| Non-repeatable read | you read the same row twice and get different values, because someone committed in between |
| Phantom read | you run the same query twice and new rows appear |
| Lost update | two transactions read stock=5, both write stock=4; one sale is lost (see Locking in the advance level) |
| Isolation | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
READ_UNCOMMITTED | possible | possible | possible |
READ_COMMITTED (PostgreSQL, Oracle, SQL Server default) | no | possible | possible |
REPEATABLE_READ (MySQL InnoDB default) | no | no | possible (InnoDB prevents most) |
SERIALIZABLE | no | no | no |
Watching the Levels Differ
Example
Transaction T1 reads the stock of product 7 three times. In between, T2 changes the stock to 7 (not yet committed) and then commits. Each isolation level shows T1 something different:
import java.util.*;
public class Main {
static int committed;
static Integer uncommitted;
public static void main(String[] args) {
for (String level : List.of("READ_UNCOMMITTED", "READ_COMMITTED", "REPEATABLE_READ")) {
committed = 10;
uncommitted = null;
Map<String, Integer> snapshot = new HashMap<>(); // T1's snapshot (REPEATABLE_READ)
int r1 = read(level, snapshot);
uncommitted = 7; // T2: update stock = 7 (not committed)
int r2 = read(level, snapshot);
committed = uncommitted; // T2: commit
uncommitted = null;
int r3 = read(level, snapshot);
System.out.printf("%-16s T1 reads: %d, %d, %d%n", level, r1, r2, r3);
}
}
static int read(String level, Map<String, Integer> snapshot) {
return switch (level) {
case "READ_UNCOMMITTED" -> uncommitted != null ? uncommitted : committed; // sees dirty data
case "READ_COMMITTED" -> committed; // latest committed
default -> snapshot.computeIfAbsent("stock", k -> committed); // first read fixed
};
}
}Output
READ_UNCOMMITTED T1 reads: 10, 7, 7 READ_COMMITTED T1 reads: 10, 10, 7 REPEATABLE_READ T1 reads: 10, 10, 10
READ_UNCOMMITTED: the second read (7) is a dirty read.READ_COMMITTED: 10, 10, 7 is a non-repeatable read.REPEATABLE_READ: 10 every time; T1 works on a consistent snapshot.
Setting the Level
@Transactional(isolation = Isolation.SERIALIZABLE)
public void closeDailyLedger(LocalDate day) { ... }
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 5) // seconds
public ReportDto salesReport() { ... }Isolation.DEFAULTuses the database default. Most apps never change it.- Higher isolation = more locking or more serialization failures to retry, so less throughput.
- Isolation does not stop lost updates on its own at READ_COMMITTED. Use optimistic locking (
@Version) or an atomic UPDATE likestock = stock - ?(advance level: Locking and Concurrency).