Learnitweb

Spring @Transactional Isolation Levels

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 LevelDescriptionPreventsDefault Behavior
    DEFAULTUses the default isolation level of the database.Depends on DB (usually READ_COMMITTED)Depends on DB
    READ_UNCOMMITTEDAllows reading uncommitted changes from other transactions.NoneMay cause dirty reads
    READ_COMMITTEDOnly committed data can be read.Dirty readsStandard default for most DBs
    REPEATABLE_READSame row read multiple times returns the same value.Dirty reads, non-repeatable readsPostgreSQL default
    SERIALIZABLEFull isolation; transactions execute sequentially.Dirty reads, non-repeatable reads, phantom readsHighest 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

    RequirementRecommended Isolation Level
    Prevent dirty reads onlyREAD_COMMITTED
    Prevent dirty & non-repeatable readsREPEATABLE_READ
    Full data consistency, prevent phantomsSERIALIZABLE
    Maximum performance, minimal lockingREAD_UNCOMMITTED

    Best Practice: Start with READ_COMMITTED (default for most DBs), increase isolation only if your use case demands stricter consistency.