Mastering Read‑Write Splitting in Spring Boot: A Complete Guide
This article explains how to implement database read‑write separation in a Spring Boot application by configuring master and slave data sources, creating a routing datasource, managing thread‑local context, and using an AOP‑based annotation to switch between read and write operations.
Introduction
In high‑concurrency scenarios, read‑write separation is a common optimization for databases: a master (write) and one or more slaves (read) reduce contention and protect the database. This article explains how to implement read‑write separation in a Spring Boot project.
1. Master‑Slave DataSource Configuration
Define two DataSource beans (master and slave) using DruidDataSourceBuilder and bind them to properties
spring.datasource.masterand
spring.datasource.slave. Then create a
DataSourceRouterthat extends
AbstractRoutingDataSourceand registers the two sources in a map.
<code>@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 tm = new DataSourceTransactionManager();
tm.setDataSource(dynamicDataSource);
return tm;
}
}
</code>2. DataSource Routing Logic
The router overrides
determineCurrentLookupKey()to obtain the current data source name from a thread‑local holder.
<code>public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
</code>3. Thread‑Local Context Holder
A simple utility that stores the current data source name in a
ThreadLocalto make the routing decision thread‑safe.
<code>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();
}
}
</code>4. Annotation and AOP for Switching
Define a
@DataSourceSwitcherannotation that specifies the target data source and whether to clear the context after execution. An AOP aspect intercepts methods annotated with this annotation, sets the context before proceeding, and optionally clears it.
<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
DataSourceEnum value() default DataSourceEnum.MASTER;
boolean clear() default true;
}
</code> <code>@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 = getMethod(pjp);
DataSourceSwitcher ds = method.getAnnotation(DataSourceSwitcher.class);
clear = ds.clear();
DataSourceContextHolder.set(ds.value().getDataSourceName());
log.info("Switching datasource 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();
}
}
</code>5. Usage Example
Apply the annotation on service or DAO methods. Read‑only methods use
DataSourceEnum.SLAVE, while write methods use
DataSourceEnum.MASTER.
<code>@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);
}
}
</code>Conclusion
Read‑write separation is achieved by configuring separate master and slave data sources, routing queries through a custom
AbstractRoutingDataSource, and managing the current data source with a thread‑local holder. The AOP‑based annotation makes switching transparent and keeps the code clean.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.