In Spring, the @Transactional
annotation is used to define transactional behavior for a method or class. One important aspect of transactions is isolation level, which determines how one transaction is isolated from others. Isolation levels help prevent data anomalies like dirty reads, non-repeatable reads, and phantom reads.
1. Understanding Isolation Levels
A transaction isolation level defines how data changes made by one transaction are visible to other concurrent transactions. Databases support different isolation levels; Spring allows you to configure them via @Transactional(isolation = ...)
.
1.1 Key Data Anomalies
- Dirty Read
- Occurs when a transaction reads uncommitted data from another transaction.
- Example: Transaction A updates a row but hasn’t committed yet. Transaction B reads that row. If A rolls back, B read invalid data.
- Non-Repeatable Read
- Occurs when a transaction reads the same row twice but gets different values because another transaction modified it in between.
- Example: Transaction A reads a value. Transaction B updates it and commits. Transaction A reads again and sees a different value.
- Phantom Read
- Occurs when a transaction reads a set of rows twice but gets different results due to insertion or deletion by another transaction.
- Example: Transaction A queries for all orders with amount > 100. Transaction B inserts a new qualifying order. Transaction A re-queries and sees the new row.
2. Isolation Levels in Spring
Spring’s @Transactional
supports all standard JDBC isolation levels via the Isolation
enum:
Isolation Level | Description | Prevents | Default Behavior |
---|---|---|---|
DEFAULT | Uses the default isolation level of the database. | Depends on DB (usually READ_COMMITTED ) | Depends on DB |
READ_UNCOMMITTED | Allows reading uncommitted changes from other transactions. | None | May cause dirty reads |
READ_COMMITTED | Only committed data can be read. | Dirty reads | Standard default for most DBs |
REPEATABLE_READ | Same row read multiple times returns the same value. | Dirty reads, non-repeatable reads | PostgreSQL default |
SERIALIZABLE | Full isolation; transactions execute sequentially. | Dirty reads, non-repeatable reads, phantom reads | Highest isolation, lowest concurrency |
2.1 READ_UNCOMMITTED
- Lowest isolation level.
- Allows dirty reads.
- Rarely used in production.
@Transactional(isolation = Isolation.READ_UNCOMMITTED) public void readUncommittedExample() { // Read data even if other transactions haven't committed }
Example Scenario: Transaction B reads a value updated by Transaction A before A commits. If A rolls back, B has read invalid data.
2.2 READ_COMMITTED
- Most commonly used isolation level.
- Prevents dirty reads.
- Allows non-repeatable reads and phantom reads.
@Transactional(isolation = Isolation.READ_COMMITTED) public void readCommittedExample() { // Only committed data is visible }
Example Scenario: Transaction B can read a row updated by Transaction A only after A commits.
2.3 REPEATABLE_READ
- Prevents dirty reads and non-repeatable reads.
- Phantom reads may still occur (depends on DB implementation).
- Ensures multiple reads of the same row in a transaction return the same value.
@Transactional(isolation = Isolation.REPEATABLE_READ) public void repeatableReadExample() { // Read same row multiple times; values are consistent }
Example Scenario: Transaction A reads a row twice. Transaction B tries to update the same row. Transaction A sees the same value both times.
2.4 SERIALIZABLE
- Highest isolation level.
- Prevents dirty reads, non-repeatable reads, and phantom reads.
- Transactions are executed as if sequentially.
- Can cause performance overhead due to locking.
@Transactional(isolation = Isolation.SERIALIZABLE) public void serializableExample() { // Full isolation }
Example Scenario: Transaction A reads a set of rows. Transaction B cannot insert/update rows that would affect Transaction A until it completes.
2.5 DEFAULT
- Uses database default isolation level.
- Good if you want Spring to delegate to the DB.
@Transactional(isolation = Isolation.DEFAULT) public void defaultIsolationExample() { // DB default applies }
3. Practical Example
Suppose you have an account transfer service:
@Service public class AccountService { @Autowired private AccountRepository accountRepository; @Transactional(isolation = Isolation.REPEATABLE_READ) public void transfer(Long fromId, Long toId, Double amount) { Account from = accountRepository.findById(fromId).orElseThrow(); Account to = accountRepository.findById(toId).orElseThrow(); from.setBalance(from.getBalance() - amount); to.setBalance(to.getBalance() + amount); accountRepository.save(from); accountRepository.save(to); } }
- Using
REPEATABLE_READ
ensures that during the transaction, the balances read will not change due to other concurrent transfers. - If
READ_COMMITTED
is used, another transaction could modify balances between reads, causing inconsistencies.
4. Choosing the Right Isolation Level
Requirement | Recommended Isolation Level |
---|---|
Prevent dirty reads only | READ_COMMITTED |
Prevent dirty & non-repeatable reads | REPEATABLE_READ |
Full data consistency, prevent phantoms | SERIALIZABLE |
Maximum performance, minimal locking | READ_UNCOMMITTED |
Best Practice: Start with READ_COMMITTED
(default for most DBs), increase isolation only if your use case demands stricter consistency.