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
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);
}
}