Implementing Database Read‑Write Splitting with Spring Boot
This article explains how to implement database read‑write splitting in a Spring Boot application by configuring master and slave data sources, creating a dynamic routing datasource, using a ThreadLocal context holder, defining a custom annotation with AOP for automatic switching, and demonstrates usage with sample code.
In high‑concurrency scenarios, read‑mostly workloads benefit from separating read and write operations across different databases. This tutorial shows how to achieve read‑write splitting in a Spring Boot project using a master (write) and a slave (read) datasource.
1. Master‑Slave DataSource Configuration – Define the master and slave datasource beans using @ConfigurationProperties and Alibaba Druid, then create a dynamic routing datasource that holds both.
/**
* 主从配置
*
* @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
* @param dynamicDataSource
* @return
* @throws Exception
*/
@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();
}
/**
* 创建sqlTemplate
* @param sqlSessionFactory
* @return
*/
@Bean
public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 事务配置
* @param dynamicDataSource
* @return
*/
@Bean(name = "dataSourceTx")
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB") DataSource dynamicDataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dynamicDataSource);
return dataSourceTransactionManager;
}
}2. DataSource Routing – Extend AbstractRoutingDataSource and override determineCurrentLookupKey to fetch the current datasource name from a ThreadLocal holder.
public class DataSourceRouter extends AbstractRoutingDataSource {
/**
* 最终的determineCurrentLookupKey返回的是从DataSourceContextHolder中拿到的,因此在动态切换数据源的时候注解
* 应该给DataSourceContextHolder设值
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}3. DataSource Context Holder – A ThreadLocal based utility that stores, retrieves, and clears the current datasource identifier.
/**
* 利用ThreadLocal封装的保存数据源上线的上下文context
*/
public class DataSourceContextHolder {
private static final ThreadLocal
context = new ThreadLocal<>();
/**
* 赋值
* @param datasourceType
*/
public static void set(String datasourceType) {
context.set(datasourceType);
}
/**
* 获取值
* @return
*/
public static String get() {
return context.get();
}
public static void clear() {
context.remove();
}
}4. Switching Annotation and AOP Configuration – Define @DataSourceSwitcher with attributes for the target datasource and whether to clear after execution. Implement an AOP aspect that intercepts methods annotated with this annotation, sets the datasource in the context holder, 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(value = 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 dataSourceSwitcher = method.getAnnotation(DataSourceSwitcher.class);
clear = dataSourceSwitcher.clear();
DataSourceContextHolder.set(dataSourceSwitcher.value().getDataSourceName());
log.info("数据源切换至:{}", dataSourceSwitcher.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 – Apply the annotation on service or DAO methods. Use @DataSourceSwitcher(DataSourceEnum.SLAVE) for read‑only methods 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 splitting reduces contention in read‑heavy applications. The core is a dynamic routing datasource that selects the appropriate datasource based on a ThreadLocal context, which is manipulated by a custom annotation and AOP. Combined with Spring’s JDBC template and transaction management, this approach provides a clean and maintainable solution for database read‑write separation.
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.