Spring @Transactional - isolation, propagation

MatBanik picture MatBanik · Dec 13, 2011 · Viewed 293.3k times · Source

Can someone explain what isolation & propagation parameters are for in the @Transactional annotation via real-world example?

Basically when and why I should choose to change their default values.

Answer

Johan Sjöberg picture Johan Sjöberg · Dec 13, 2011

Good question, although not a trivial one to answer.

Propagation

Defines how transactions relate to each other. Common options:

  • Required: Code will always run in a transaction. Creates a new transaction or reuses one if available.
  • Requires_new: Code will always run in a new transaction. Suspends the current transaction if one exists.

Isolation

Defines the data contract between transactions.

  • Read Uncommitted: Allows dirty reads.
  • Read Committed: Does not allow dirty reads.
  • Repeatable Read: If a row is read twice in the same transaction, the result will always be the same.
  • Serializable: Performs all transactions in a sequence.

The different levels have different performance characteristics in a multi-threaded application. I think if you understand the dirty reads concept you will be able to select a good option.


Example of when a dirty read can occur:

  thread 1   thread 2      
      |         |
    write(x)    |
      |         |
      |        read(x)
      |         |
    rollback    |
      v         v 
           value (x) is now dirty (incorrect)

So a sane default (if such can be claimed) could be Read Committed, which only lets you read values which have already been committed by other running transactions, in combination with a propagation level of Required. Then you can work from there if your application has other needs.


A practical example of where a new transaction will always be created when entering the provideService routine and completed when leaving:

public class FooService {
    private Repository repo1;
    private Repository repo2;

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void provideService() {
        repo1.retrieveFoo();
        repo2.retrieveFoo();
    }
}

Had we instead used Required, the transaction would remain open if the transaction was already open when entering the routine. Note also that the result of a rollback could be different as several executions could take part in the same transaction.


We can easily verify the behaviour with a test and see how results differ with propagation levels:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:/fooService.xml")
public class FooServiceTests {

    private @Autowired TransactionManager transactionManager;
    private @Autowired FooService fooService;

    @Test
    public void testProvideService() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        fooService.provideService();
        transactionManager.rollback(status);
        // assert repository values are unchanged ... 
}

With a propagation level of

  • Requires new: we would expect fooService.provideService() was NOT rolled back since it created it's own sub-transaction.

  • Required: we would expect everything was rolled back and the backing store was unchanged.