Databases 11 min read

Implementing Database Read‑Write Splitting with Spring Boot

This article explains how to achieve read‑write separation in high‑concurrency Java applications by configuring master‑slave data sources, routing with AbstractRoutingDataSource, managing context via ThreadLocal, defining a custom @DataSourceSwitcher annotation, and applying AOP to switch databases at runtime.

Top Architect
Top Architect
Top Architect
Implementing Database Read‑Write Splitting with Spring Boot

In high‑concurrency scenarios, common database optimization techniques include read‑write separation, caching, master‑slave clusters, and sharding. Most internet applications follow a "read‑many, write‑few" pattern, which makes using a master (write) and a slave (read) database a practical solution.

The master handles all write operations, while the slave handles read operations. By isolating read and write traffic, you can reduce contention, relieve load on the master, and protect the database. This article focuses on implementing read‑write separation.

1. Configuring Master‑Slave Data Sources

Data source properties are defined in the application configuration file and bound to Java classes using @ConfigurationProperties . The master is typically named master and the slave slave . The project uses Alibaba Druid as the connection pool and builds DataSource objects via the builder pattern.

/**
 * Master‑slave configuration
 * @author wyq
 */
@Configuration
@MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {

    /** Master data source */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource master() {
        return DruidDataSourceBuilder.create().build();
    }

    /** Slave data source */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaver() {
        return DruidDataSourceBuilder.create().build();
    }

    /** Instantiate data source router */
    @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;
    }

    @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 tx = new DataSourceTransactionManager();
        tx.setDataSource(dynamicDataSource);
        return tx;
    }
}

2. Configuring Data Source Routing

The routing component extends AbstractRoutingDataSource and overrides determineCurrentLookupKey to obtain the current data source name from a context holder.

public class DataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

3. Data Source Context Holder

A ThreadLocal based holder stores the current data source name, providing set , get , and clear methods to manage the context safely across threads.

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. Switching Annotation and AOP Configuration

The custom @DataSourceSwitcher annotation specifies which data source to use and whether to clear the context after method execution.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
    DataSourceEnum value() default DataSourceEnum.MASTER;
    boolean clear() default true;
}

The AOP aspect intercepts methods annotated with @DataSourceSwitcher , sets the appropriate data source in the context holder, proceeds with the method, and optionally clears the context.

@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 data source 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 Example and Testing

In service or DAO layers, annotate methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) for read operations 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 separation is achieved by configuring master and slave data sources, routing requests through a custom AbstractRoutingDataSource , and managing the current data source with a ThreadLocal context holder. Combined with Spring AOP and a simple annotation, developers can transparently direct read and write operations to the appropriate database, improving performance and scalability.

JavaAOPDatabaseSpring BootRead-Write SplittingDataSource Routing
Top Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

login 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.