Mastering Spring Boot Read‑Write Splitting with AbstractRoutingDataSource
This article explains how to implement read‑write separation in Spring Boot by extending AbstractRoutingDataSource, using ThreadLocal for key storage, and creating AOP aspects that prioritize datasource switching over transaction management.
While learning Spring Boot, the author built a read‑write separation solution from scratch, avoiding copy‑paste from online articles.
The basic idea is to route read operations to a read‑only database and write operations to the primary database, ensuring the datasource is chosen before Spring commits a transaction.
To achieve this, an AOP aspect with a higher precedence than Spring's transaction aspect is created; two custom annotations mark read and write methods.
Understanding AbstractRoutingDataSource
AbstractRoutingDataSource is the key class. It holds a Map<Object, DataSource> called resolvedDataSources. The method determineTargetDataSource() obtains a lookup key via determineCurrentLookupKey() and retrieves the corresponding DataSource from the map.
Important methods: setTargetDataSources – sets the candidate datasource map. setDefaultTargetDataSource – sets the fallback datasource. determineCurrentLookupKey – returns the key used for routing.
During bean initialization, afterPropertiesSet() converts targetDataSources and defaultTargetDataSource into resolvedDataSources and resolvedDefaultDataSource. Therefore, these setters must be called before afterPropertiesSet().
When unfamiliar with a technology, first search for essential knowledge, then experiment.
Thread‑local key storage
A DataSourceContextHolder class uses a ThreadLocal<String> to store the current datasource key (read or write) for each thread.
public class DataSourceContextHolder {
private static final ThreadLocal<String> local = new ThreadLocal<>();
public static void setRead() { local.set(DataSourceType.read.name()); }
public static void setWrite() { local.set(DataSourceType.write.name()); }
public static String getReadOrWrite() { return local.get(); }
}AOP aspect for datasource switching
@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAopAspect implements PriorityOrdered {
@Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) and @annotation(ReadDataSource)")
public void setReadDataSourceType() { DataSourceContextHolder.setRead(); }
@Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) and @annotation(WriteDataSource)")
public void setWriteDataSourceType() { DataSourceContextHolder.setWrite(); }
@Override
public int getOrder() { return 1; } // higher priority than transaction
}Custom routing datasource implementation
@Component
public class RoutingDataSouceImpl extends AbstractRoutingDataSource {
@Autowired @Qualifier("writeDataSource") private DataSource writeDataSource;
@Autowired @Qualifier("readDataSource") private DataSource readDataSource;
@Override
public void afterPropertiesSet() {
this.setDefaultTargetDataSource(writeDataSource);
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.write.name(), writeDataSource);
targetDataSources.put(DataSourceType.read.name(), readDataSource);
this.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getReadOrWrite();
Assert.notNull(typeKey, "Database routing key is null");
return typeKey;
}
}Bean configuration
@Primary
@Bean(name="writeDataSource", destroyMethod="close")
@ConfigurationProperties(prefix="test_write")
public DataSource writeDataSource() { return new DruidDataSource(); }
@Bean(name="readDataSource", destroyMethod="close")
@ConfigurationProperties(prefix="test_read")
public DataSource readDataSource() { return new DruidDataSource(); }
@Bean(name="writeOrReadsqlSessionFactory")
public SqlSessionFactory sqlSessionFactorys(RoutingDataSouceImpl roundRobinDataSouceProxy) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(roundRobinDataSouceProxy);
bean.setTypeAliasesPackage("com.springboot.demo.mybatis.model");
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return bean.getObject();
}
@Bean(name="writeOrReadTransactionManager")
public DataSourceTransactionManager transactionManager(RoutingDataSouceImpl roundRobinDataSouceProxy) {
return new DataSourceTransactionManager(roundRobinDataSouceProxy);
}With these components, the read‑write routing logic is complete; the remaining configuration (e.g., MyBatis mapper files) follows standard Spring Boot practices.
In summary, mastering AbstractRoutingDataSource makes implementing Spring Boot read‑write separation straightforward.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
