Implementing Database Read‑Write Separation with Spring Boot
This article explains how to achieve database read‑write separation in high‑concurrency scenarios using Spring Boot, covering the concepts of master‑slave architecture, configuration of multiple data sources, routing logic with AbstractRoutingDataSource, context management, custom annotations, AOP handling, and practical usage examples with code snippets.
In high‑concurrency environments, read‑write separation is a common optimization technique that separates write operations to a master database and read operations to one or more slave databases, reducing contention and improving performance.
The article first introduces the basic ideas of master‑slave replication, its benefits, and its limitations such as replication lag, then moves on to a concrete implementation using Java and Spring Boot.
1. Data Source Configuration
The project depends on Spring Boot, Spring AOP, Spring JDBC, and AspectJ Weaver. The master and slave data sources are defined in the application properties and bound to Java beans using @ConfigurationProperties . Druid is used as the connection pool.
/**
* 主从配置
*/
@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. Routing Data Source
A custom router extends AbstractRoutingDataSource and overrides determineCurrentLookupKey() to fetch the current data source name from a thread‑local holder.
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}3. DataSource Context Holder
This utility uses ThreadLocal to store the current data source identifier, providing set , get , and clear methods to manage the context safely.
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. Custom Annotation and AOP
A @DataSourceSwitcher annotation specifies the target data source (MASTER or SLAVE) and whether to clear the context after method execution. An AOP aspect intercepts methods annotated with this annotation, sets the appropriate data source 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;
}
@Aspect
@Component
public class DataSourceContextAop {
@Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")
public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
DataSourceSwitcher ds = method.getAnnotation(DataSourceSwitcher.class);
boolean clear = ds.clear();
DataSourceContextHolder.set(ds.value().getDataSourceName());
try {
return pjp.proceed();
} finally {
if (clear) { DataSourceContextHolder.clear(); }
}
}
}5. Usage Example
In service or DAO layers, annotate read‑only methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) and write methods with @DataSourceSwitcher(DataSourceEnum.MASTER) . The aspect automatically switches the underlying data source for each call.
@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
The guide demonstrates a complete solution for database read‑write separation in a Spring Boot application, emphasizing the importance of a routing data source, a thread‑local context holder, custom annotations, and AOP to transparently direct read and write operations to the appropriate databases.
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.