Mastering Java Read‑Write Splitting: Configure Master‑Slave DataSources with Spring
This article explains the concept of database read‑write splitting, discusses its limitations, and provides a step‑by‑step guide with Spring Boot code to configure master‑slave data sources, implement dynamic routing, context handling, custom annotations, and AOP for seamless switching.
Preface
First, consider the question: in high‑concurrency scenarios, what database optimization techniques are available? Common methods include read‑write separation, caching, master‑slave clustering, and sharding. Most internet applications are read‑heavy, so a typical setup uses a master for writes and a slave for reads, possibly with a read‑only cluster to reduce conflicts and protect the database.
The article focuses on read‑write separation and how to implement it.
1. Master‑Slave DataSource Configuration
We configure master and slave databases, usually via configuration files. Using @ConfigurationProperties, properties from application.properties are mapped to Java classes. The master is named master, the slave slave. The project uses Alibaba Druid connection pool and the builder pattern to create DataSource objects, then configures SqlSessionFactory, SqlSessionTemplate, and transaction manager.
@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<Object, Object> 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 manager = new DataSourceTransactionManager();
manager.setDataSource(dynamicDataSource);
return manager;
}
}2. DataSource Routing Configuration
Routing is essential for read‑write separation. Spring’s AbstractRoutingDataSource selects the current data source based on a rule. We implement determineCurrentLookupKey() to fetch the key from a global DataSourceContextHolder, which stores the current data source name.
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}3. DataSource Context Holder
The context holder uses ThreadLocal to store the current data source name, providing set, get, and clear methods to manage the value safely across threads.
public class DataSourceContextHolder {
private static final ThreadLocal<String> 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. Switch Annotation and AOP Configuration
Define a @DataSourceSwitcher annotation with attributes for the target data source and whether to clear the context after execution. An AOP aspect intercepts methods annotated with this annotation, 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;
} @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("Data source switched 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 and Testing
After configuring read‑write separation, apply the annotation on service or DAO methods. Use @DataSourceSwitcher(DataSourceEnum.SLAVE) for read‑only methods and @DataSourceSwitcher(DataSourceEnum.MASTER) for write operations. An automatic approach can switch based on method name prefixes (e.g., select, update).
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@DataSourceSwitcher(DataSourceEnum.SLAVE)
public List<Order> getOrder(String orderId) {
return orderMapper.listOrders(orderId);
}
@DataSourceSwitcher(DataSourceEnum.MASTER)
public List<Order> insertOrder(Long orderId) {
Order order = new Order();
order.setOrderId(orderId);
return orderMapper.saveOrder(order);
}
}6. Summary
The core of read‑write separation lies in dynamic data source routing: extend AbstractRoutingDataSource, override determineCurrentLookupKey, and manage the current data source via a ThreadLocal context holder. Combined with Spring’s JDBC template, transaction management, and AOP, a robust master‑slave setup can be achieved.
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.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
