Mastering Spring Boot Read‑Write Splitting with AbstractRoutingDataSource

This article explains how to implement read‑write separation in Spring Boot by extending AbstractRoutingDataSource, using ThreadLocal for key storage, and creating AOP aspects that prioritize datasource switching over transaction management.

Programmer DD
Programmer DD
Programmer DD
Mastering Spring Boot Read‑Write Splitting with AbstractRoutingDataSource

While learning Spring Boot, the author built a read‑write separation solution from scratch, avoiding copy‑paste from online articles.

The basic idea is to route read operations to a read‑only database and write operations to the primary database, ensuring the datasource is chosen before Spring commits a transaction.

To achieve this, an AOP aspect with a higher precedence than Spring's transaction aspect is created; two custom annotations mark read and write methods.

Understanding AbstractRoutingDataSource

AbstractRoutingDataSource is the key class. It holds a Map<Object, DataSource> called resolvedDataSources. The method determineTargetDataSource() obtains a lookup key via determineCurrentLookupKey() and retrieves the corresponding DataSource from the map.

Important methods: setTargetDataSources – sets the candidate datasource map. setDefaultTargetDataSource – sets the fallback datasource. determineCurrentLookupKey – returns the key used for routing.

During bean initialization, afterPropertiesSet() converts targetDataSources and defaultTargetDataSource into resolvedDataSources and resolvedDefaultDataSource. Therefore, these setters must be called before afterPropertiesSet().

When unfamiliar with a technology, first search for essential knowledge, then experiment.

Thread‑local key storage

A DataSourceContextHolder class uses a ThreadLocal<String> to store the current datasource key (read or write) for each thread.

public class DataSourceContextHolder {
    private static final ThreadLocal<String> local = new ThreadLocal<>();
    public static void setRead() { local.set(DataSourceType.read.name()); }
    public static void setWrite() { local.set(DataSourceType.write.name()); }
    public static String getReadOrWrite() { return local.get(); }
}

AOP aspect for datasource switching

@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAopAspect implements PriorityOrdered {
    @Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) and @annotation(ReadDataSource)")
    public void setReadDataSourceType() { DataSourceContextHolder.setRead(); }
    @Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) and @annotation(WriteDataSource)")
    public void setWriteDataSourceType() { DataSourceContextHolder.setWrite(); }
    @Override
    public int getOrder() { return 1; } // higher priority than transaction
}

Custom routing datasource implementation

@Component
public class RoutingDataSouceImpl extends AbstractRoutingDataSource {
    @Autowired @Qualifier("writeDataSource") private DataSource writeDataSource;
    @Autowired @Qualifier("readDataSource") private DataSource readDataSource;
    @Override
    public void afterPropertiesSet() {
        this.setDefaultTargetDataSource(writeDataSource);
        Map<Object,Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.write.name(), writeDataSource);
        targetDataSources.put(DataSourceType.read.name(), readDataSource);
        this.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getReadOrWrite();
        Assert.notNull(typeKey, "Database routing key is null");
        return typeKey;
    }
}

Bean configuration

@Primary
@Bean(name="writeDataSource", destroyMethod="close")
@ConfigurationProperties(prefix="test_write")
public DataSource writeDataSource() { return new DruidDataSource(); }

@Bean(name="readDataSource", destroyMethod="close")
@ConfigurationProperties(prefix="test_read")
public DataSource readDataSource() { return new DruidDataSource(); }

@Bean(name="writeOrReadsqlSessionFactory")
public SqlSessionFactory sqlSessionFactorys(RoutingDataSouceImpl roundRobinDataSouceProxy) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(roundRobinDataSouceProxy);
    bean.setTypeAliasesPackage("com.springboot.demo.mybatis.model");
    bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
    return bean.getObject();
}

@Bean(name="writeOrReadTransactionManager")
public DataSourceTransactionManager transactionManager(RoutingDataSouceImpl roundRobinDataSouceProxy) {
    return new DataSourceTransactionManager(roundRobinDataSouceProxy);
}

With these components, the read‑write routing logic is complete; the remaining configuration (e.g., MyBatis mapper files) follows standard Spring Boot practices.

In summary, mastering AbstractRoutingDataSource makes implementing Spring Boot read‑write separation straightforward.

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.

aopSpring Bootread/write splittingThreadLocalDataSource Routingabstractroutingdatasource
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.