Implementing Database Read‑Write Splitting with Spring Boot
This article explains how to achieve database read‑write separation in high‑concurrency Java applications by configuring master‑slave data sources, implementing a routing data source, managing context with ThreadLocal, defining a custom @DataSourceSwitcher annotation, and applying AOP to dynamically switch between read and write databases.
In high‑concurrency scenarios, read‑heavy workloads benefit from separating database read and write operations. The article introduces common optimization methods such as read‑write splitting, caching, and master‑slave clustering, focusing on the implementation of read‑write separation using Spring Boot.
1. Master‑Slave Data Source Configuration – Define master (write) and slave (read) data sources using @ConfigurationProperties and Druid connection pool, then create a dynamic routing data source that holds both sources.
/**
* 主从配置
* @author wyq
*/
@Configuration
@MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource master() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaver() {
return DruidDataSourceBuilder.create().build();
}
@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;
}
// sessionFactory, sqlTemplate and transaction manager beans omitted for brevity
}2. Data Source Router – Extend AbstractRoutingDataSource and override 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 – Use a ThreadLocal<String> to store the current data source identifier, providing set , get , and clear methods.
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. @DataSourceSwitcher Annotation and AOP – Define an annotation with attributes for the target data source and whether to clear the context after execution. Implement an AOP aspect that intercepts methods annotated with @DataSourceSwitcher, sets the appropriate data source in the context, proceeds with the method, and optionally clears the context.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
DataSourceEnum value() default DataSourceEnum.MASTER;
boolean clear() default true;
}
@Aspect
@Component
public class DataSourceContextAop {
@Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")
public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
boolean clear = false;
try {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
DataSourceSwitcher ds = method.getAnnotation(DataSourceSwitcher.class);
clear = ds.clear();
DataSourceContextHolder.set(ds.value().getDataSourceName());
return pjp.proceed();
} finally {
if (clear) { DataSourceContextHolder.clear(); }
}
}
}5. Usage Example – Apply @DataSourceSwitcher on service methods to direct read operations to the slave and write operations to the master. The aspect automatically switches the data source based on the annotation.
@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);
}
}The core of read‑write splitting lies in the routing mechanism provided by AbstractRoutingDataSource and the global context manager DataSourceContextHolder . Combined with Spring's JDBC template, transaction management, and AOP, the solution enables transparent switching between read and write databases, improving performance and reducing contention in high‑traffic applications.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.