Backend Development 11 min read

Implementing Database Read‑Write Splitting with Spring Boot and Dynamic DataSource Routing

This article explains how to achieve database read‑write separation in high‑concurrency Java applications by configuring master and slave data sources, implementing a routing DataSource, managing context with ThreadLocal, defining a custom @DataSourceSwitcher annotation, and applying AOP to switch between read and write operations.

Top Architect
Top Architect
Top Architect
Implementing Database Read‑Write Splitting with Spring Boot and Dynamic DataSource Routing

The article begins by discussing common database optimization techniques for high‑concurrency scenarios, focusing on read‑write separation where the master handles writes and one or more slaves handle reads.

It then introduces the required dependencies (Spring Boot, Spring AOP, Spring JDBC, AspectJ Weaver) and shows how to configure master and slave data sources using @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;
    }
    // sessionFactory, sqlTemplate, transactionManager beans omitted for brevity
}

The routing logic is implemented by extending AbstractRoutingDataSource and overriding determineCurrentLookupKey() to fetch the current data source name from a ThreadLocal holder.

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

A DataSourceContextHolder class provides static set , get , and clear methods to manage the data source name per thread.

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 allows developers to specify the desired data source (MASTER or SLAVE) on service or DAO methods.

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

The accompanying AOP aspect intercepts methods annotated with @DataSourceSwitcher , sets the appropriate data source in the context before method execution, and optionally clears it afterwards.

@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 = ((MethodSignature) pjp.getSignature()).getMethod();
            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(); }
        }
    }
}

Usage examples show how to annotate read methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) and write methods with @DataSourceSwitcher(DataSourceEnum.MASTER) , enabling automatic routing of queries to the appropriate database.

@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 dynamic routing mechanism, the ThreadLocal context holder, and proper AOP configuration, which together provide a clean and maintainable solution for high‑performance database access.

JavaAOPSpring BootMySQLRead-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.