Mastering Read‑Write Splitting in Spring Boot: A Complete Guide
This article explains why read‑write separation is essential for high‑concurrency applications, discusses its limitations, and provides a step‑by‑step Spring Boot implementation—including datasource configuration, routing logic, context management, custom annotations, AOP handling, and usage examples—complete with code snippets and diagrams.
Introduction
In high‑concurrency scenarios, common database optimization techniques include read‑write separation, caching, master‑slave clusters, and sharding. Most internet applications are read‑heavy, so a typical setup uses a master database for writes and one or more slave databases for reads, reducing read‑write conflicts, relieving load, and protecting the database.
Limitations of Master‑Slave Replication
Replication delay can cause stale reads when a write has not yet been synchronized to the slave. A simple fix is to route the read to the master temporarily, but this defeats the purpose of separation and may not meet strict consistency requirements.
Implementation Overview
The implementation relies on Spring Boot with dependencies such as spring‑boot, spring‑aop, spring‑jdbc, and aspectjweaver. The following sections detail the configuration and code.
1. Master‑Slave DataSource Configuration
Configuration properties are mapped using @ConfigurationProperties. The master datasource is named master and the slave datasource slave. Druid is used as the connection pool, and beans for DataSource, SqlSessionFactory, SqlSessionTemplate, and transaction management are defined.
@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(name = "dataSourceTx")
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB") DataSource dynamicDataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dynamicDataSource);
return tm;
}
@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);
}
}2. DataSource Router Configuration
The router extends AbstractRoutingDataSource and overrides determineCurrentLookupKey to obtain the current datasource name from DataSourceContextHolder.
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}3. DataSource Context Holder
This class uses a ThreadLocal<String> to store the current datasource name, providing set, get, and clear methods.
public class DataSourceContextHolder {
private static final ThreadLocal<String> context = new ThreadLocal<>();
/** Set the current datasource */
public static void set(String datasourceType) {
context.set(datasourceType);
}
/** Get the current datasource */
public static String get() {
return context.get();
}
/** Clear the ThreadLocal to avoid memory leaks */
public static void clear() {
context.remove();
}
}4. Switch Annotation and AOP Configuration
A custom annotation @DataSourceSwitcher defines the target datasource (default MASTER) and whether to clear the context after execution. An AOP aspect intercepts methods annotated with this annotation, sets the datasource in the context before proceeding, and optionally clears it afterwards.
@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("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();
}
}5. Usage and Testing
After configuring read‑write separation, annotate service or DAO methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) for read operations and @DataSourceSwitcher(DataSourceEnum.MASTER) for write operations. An alternative approach automatically switches 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 routing: extending AbstractRoutingDataSource, providing a global DataSourceContextHolder, and using custom annotations with AOP to switch the datasource at runtime. Combined with Spring’s JDBC template, transaction management, and mapper configuration, a robust read‑write split architecture is 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.
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.
