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.

Architect's Guide
Architect's Guide
Architect's Guide
Mastering Read‑Write Splitting in Spring Boot: A Complete Guide

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.

Read‑write separation diagram
Read‑write separation diagram
Read‑write separation flowchart
Read‑write separation flowchart
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaaopBackend Developmentread/write splittingDataSource Routing
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.