Search code examples
springspring-boottransactionsspring-dataspring-transactions

SPR-16876 TransactionSynchronizationManager not returning correct values in implementation of AbstractRoutingDataSource


I am following this link https://vladmihalcea.com/read-write-read-only-transaction-routing-spring/ to set up the database master and replica instance in my spring boot app. The only difference is I am not using Hibernate or JPA.

Found out issue already exist in Spring Framework. Please refer to this https://jira.spring.io/browse/SPR-16876?redirect=false

My service class is annotated with @Transactional(readOnly = true).

But in the implementation of RoutingDataSource, I always get TransactionSynchronizationManager.isCurrentTransactionReadOnly() as false and TransactionSynchronizationManager.getCurrentTransactionName() as null in determineCurrentLookupKey().

@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {
    public enum Route {
        PRIMARY, REPLICA
    }

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        log.info("ReadOnly? " + TransactionSynchronizationManager.isCurrentTransactionReadOnly()); //Returns false always
        log.info("Tx Name " + (TransactionSynchronizationManager.getCurrentTransactionName())); //Returns null always
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? Route.REPLICA : Route.PRIMARY;
    }
}

If I log the same in my service method, I get TransactionSynchronizationManager.isCurrentTransactionReadOnly() as true and TransactionSynchronizationManager.getCurrentTransactionName() with a not-null value.

@Service
@Transactional(readOnly = true)
public class DatabaseTestingService {
    public List<String> replica() {
        System.out.println("--> " + (TransactionSynchronizationManager.isCurrentTransactionReadOnly())); //Returns True
        System.out.println("--> " + (TransactionSynchronizationManager.getCurrentTransactionName())); //Returns Trx name
        ...
    }
}

As @Transactional(readOnly = true) is set on my service class, it should default call to the read-only db instance but it is always calling read-write db instance


Solution

  • I looked into the AbstractPlatformTransactionManager.java. The logic to start new transaction is written inside the startTransaction() as below

    private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
            boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
    
        boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
        DefaultTransactionStatus status = newTransactionStatus(
                definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
        doBegin(transaction, definition); //Transaction is getting built here and connection is getting acquired here based on RoutingDataSource.determineCurrentLookupKey()
        prepareSynchronization(status, definition); //Transaction is getting synchronized after the connection is built
        return status;
    }
    

    As you can see in the above code transaction is getting synchronized after connection is acquired. So RoutingDataSource.determineCurrentLookupKey() will not be called after prepareSynchronization(status, definition).

    To synchronize the transaction before the connection is getting acquired I extended the DataSourceTransactionManager and wrote logic as below

    @Component
    @Slf4j
    public class CustomTransactionManager extends DataSourceTransactionManager {
    
        private static final long serialVersionUID = 1L;
    
        public CustomTransactionManager(DataSource dataSource) {
            super(dataSource);
        }
    
        @Override
        protected void doBegin(Object transaction, TransactionDefinition definition) {
            if (definition.getName().contains("package_substring")) {
                log.debug(String.format("Init. Transaction for MyApp Service: %s : %s", definition.getName(),
                        definition));
                TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
                        definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT
                        ? definition.getIsolationLevel()
                                : null);
                TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
                TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
            }
            super.doBegin(transaction, definition);
        }
    }