Implementing Database Read‑Write Splitting with Spring Boot
This article explains how to achieve read‑write separation in high‑concurrency Java applications by configuring master‑slave data sources, routing with AbstractRoutingDataSource, managing context via ThreadLocal, defining a custom @DataSourceSwitcher annotation, and applying AOP to switch databases at runtime.
In high‑concurrency scenarios, common database optimization techniques include read‑write separation, caching, master‑slave clusters, and sharding. Most internet applications follow a "read‑many, write‑few" pattern, which makes using a master (write) and a slave (read) database a practical solution.
The master handles all write operations, while the slave handles read operations. By isolating read and write traffic, you can reduce contention, relieve load on the master, and protect the database. This article focuses on implementing read‑write separation.
1. Configuring Master‑Slave Data Sources
Data source properties are defined in the application configuration file and bound to Java classes using @ConfigurationProperties . The master is typically named master and the slave slave . The project uses Alibaba Druid as the connection pool and builds DataSource objects via the builder pattern.
/**
* Master‑slave configuration
* @author wyq
*/
@Configuration
@MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {
/** Master data source */
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource master() {
return DruidDataSourceBuilder.create().build();
}
/** Slave data source */
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaver() {
return DruidDataSourceBuilder.create().build();
}
/** Instantiate data source router */
@Bean
public DataSourceRouter dynamicDB(@Qualifier("master") DataSource masterDataSource,
@Autowired(required = false) @Qualifier("slaver") DataSource slaveDataSource) {
DataSourceRouter dynamicDataSource = new DataSourceRouter();
Map
targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
if (slaveDataSource != null) {
targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
}
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sessionFactory(@Qualifier("dynamicDB") DataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
bean.setDataSource(dynamicDataSource);
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "dataSourceTx")
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB") DataSource dynamicDataSource) {
DataSourceTransactionManager tx = new DataSourceTransactionManager();
tx.setDataSource(dynamicDataSource);
return tx;
}
}2. Configuring Data Source Routing
The routing component extends AbstractRoutingDataSource and overrides determineCurrentLookupKey to obtain the current data source name from a context holder.
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}3. Data Source Context Holder
A ThreadLocal based holder stores the current data source name, providing set , get , and clear methods to manage the context safely across threads.
public class DataSourceContextHolder {
private static final ThreadLocal
context = new ThreadLocal<>();
public static void set(String datasourceType) {
context.set(datasourceType);
}
public static String get() {
return context.get();
}
public static void clear() {
context.remove();
}
}4. Switching Annotation and AOP Configuration
The custom @DataSourceSwitcher annotation specifies which data source to use and whether to clear the context after method execution.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
DataSourceEnum value() default DataSourceEnum.MASTER;
boolean clear() default true;
}The AOP aspect intercepts methods annotated with @DataSourceSwitcher , sets the appropriate data source in the context holder, proceeds with the method, and optionally clears the context.
@Slf4j
@Aspect
@Order(1)
@Component
public class DataSourceContextAop {
@Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")
public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
boolean clear = false;
try {
Method method = this.getMethod(pjp);
DataSourceSwitcher ds = method.getAnnotation(DataSourceSwitcher.class);
clear = ds.clear();
DataSourceContextHolder.set(ds.value().getDataSourceName());
log.info("Switching data source to: {}", ds.value().getDataSourceName());
return pjp.proceed();
} finally {
if (clear) {
DataSourceContextHolder.clear();
}
}
}
private Method getMethod(JoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
return signature.getMethod();
}
}5. Usage Example and Testing
In service or DAO layers, annotate methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) for read operations and @DataSourceSwitcher(DataSourceEnum.MASTER) for write operations.
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@DataSourceSwitcher(DataSourceEnum.SLAVE)
public List
getOrder(String orderId) {
return orderMapper.listOrders(orderId);
}
@DataSourceSwitcher(DataSourceEnum.MASTER)
public List
insertOrder(Long orderId) {
Order order = new Order();
order.setOrderId(orderId);
return orderMapper.saveOrder(order);
}
}6. Summary
Read‑write separation is achieved by configuring master and slave data sources, routing requests through a custom AbstractRoutingDataSource , and managing the current data source with a ThreadLocal context holder. Combined with Spring AOP and a simple annotation, developers can transparently direct read and write operations to the appropriate database, improving performance and scalability.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.