Databases 12 min read

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

This article explains how to achieve database read‑write separation in high‑concurrency scenarios using Spring Boot, Druid connection pool, dynamic DataSource routing, a ThreadLocal context holder, custom annotations, and AOP, providing complete configuration code and usage examples for both read and write operations.

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

Preface

The author, a senior architect, introduces the problem of optimizing databases under high concurrency, mentioning common techniques such as read‑write separation, caching, master‑slave clustering, and sharding. The focus of this article is on implementing read‑write separation.

1: Master‑Slave DataSource Configuration

The master database handles writes while the slave handles reads. Configuration is done via @ConfigurationProperties mapping properties from application.properties to Java beans, using Alibaba Druid as the connection pool.

/**
 * 主从配置
 * @author wyq
 */
@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 dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dynamicDataSource);
        return dataSourceTransactionManager;
    }
}

2: DataSource Routing Configuration

The routing class extends AbstractRoutingDataSource and overrides determineCurrentLookupKey() to fetch the current data source name from a ThreadLocal holder.

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

3: DataSource Context

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

    /** Assign value */
    public static void set(String datasourceType) {
        context.set(datasourceType);
    }

    /** Retrieve value */
    public static String get() {
        return context.get();
    }

    /** Clear value */
    public static void clear() {
        context.remove();
    }
}

4: Switch Annotation and AOP Configuration

A custom @DataSourceSwitcher annotation defines the target data source and whether to clear the context after execution. An AOP aspect intercepts methods annotated with this annotation, sets the appropriate data source in the context, proceeds with the method, and optionally clears the context.

@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 dataSourceSwitcher = method.getAnnotation(DataSourceSwitcher.class);
            clear = dataSourceSwitcher.clear();
            DataSourceContextHolder.set(dataSourceSwitcher.value().getDataSourceName());
            log.info("Data source switched to: {}", dataSourceSwitcher.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

In service or DAO layers, annotate methods with @DataSourceSwitcher(DataSourceEnum.SLAVE) for read operations and @DataSourceSwitcher(DataSourceEnum.MASTER) for write operations. The aspect ensures the correct data source is used at runtime.

@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

The article demonstrates a complete solution for database read‑write separation by configuring master and slave data sources, implementing a routing DataSource that consults a ThreadLocal context holder, and using custom annotations with AOP to switch data sources transparently during method execution.

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