javaspringtransactionsisolationpropagation

Spring @Transactional - isolation, propagation


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

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


Solution

  • Good question, although not a trivial one to answer.

    Propagation

    Defines how transactions relate to each other. Common options:

    The default value for @Transactional is REQUIRED, and this is often what you want.

    Isolation

    Defines the data contract between transactions.

    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.

    Defaults may vary between difference databases. As an example, for MariaDB it is REPEATABLE READ.


    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 ISOLATION_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