Backend Development 10 min read

Implementing Database Read‑Write Separation with Spring Boot

This article explains how to achieve database read‑write separation in high‑concurrency Java applications by configuring master‑slave data sources, implementing a routing datasource, managing context with ThreadLocal, and using custom annotations with AOP to switch between read and write databases at runtime.

Architect's Guide
Architect's Guide
Architect's Guide
Implementing Database Read‑Write Separation with Spring Boot

The article begins by discussing the need for read‑write separation in high‑concurrency scenarios, describing common optimization techniques such as read‑write splitting, caching, master‑slave clustering, and sharding, and noting the typical read‑heavy nature of internet applications.

It then introduces the practical implementation using Java and Spring Boot, listing required dependencies (spring‑boot, spring‑aop, spring‑jdbc, aspectjweaver) and explaining the configuration of master and slave data sources via @ConfigurationProperties and Alibaba Druid 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;
    }

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

A custom DataSourceRouter extends AbstractRoutingDataSource and overrides determineCurrentLookupKey() to obtain the current datasource name from a thread‑local holder.

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

The DataSourceContextHolder class uses a ThreadLocal<String> to store, retrieve, and clear the current datasource identifier, ensuring thread‑safety.

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(); }
}

A custom annotation @DataSourceSwitcher defines the target datasource (master by default) 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, logs the switch, and optionally clears the context in a finally block.

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

@Aspect
@Component
@Order(1)
public class DataSourceContextAop {
    @Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")
    public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
        boolean clear = false;
        try {
            Method method = getMethod(pjp);
            DataSourceSwitcher ds = method.getAnnotation(DataSourceSwitcher.class);
            clear = ds.clear();
            DataSourceContextHolder.set(ds.value().getDataSourceName());
            log.info("Data source switched to: {}", ds.value().getDataSourceName());
            return pjp.proceed();
        } finally {
            if (clear) { DataSourceContextHolder.clear(); }
        }
    }
    private Method getMethod(JoinPoint pjp) {
        return ((MethodSignature) pjp.getSignature()).getMethod();
    }
}

Usage examples show how to annotate service methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) for read operations and @DataSourceSwitcher(DataSourceEnum.MASTER) for write operations, allowing seamless routing without manual datasource handling.

@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);
    }
}

The article concludes that the core of read‑write separation lies in the routing datasource and the global context holder; combined with Spring’s JDBC template, transaction management, and AOP, a robust read‑write split can be achieved in a Spring Boot application.

backendJavaAOPDatabaseSpring BootRead-Write SeparationDataSource 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

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.