Backend Development 17 min read

Implementing Read‑Write Splitting in Spring with AbstractRoutingDataSource and AOP

This article explains how to achieve database read‑write separation in a Spring application by using AbstractRoutingDataSource for routing, ThreadLocal for context, AOP interceptors for automatic switching, and provides a complete example with configuration, service code, and test cases.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Implementing Read‑Write Splitting in Spring with AbstractRoutingDataSource and AOP

Most systems perform many reads and few writes, so a master‑slave setup can reduce database load by directing write operations to the master and read operations to one or more slaves.

The goal is to implement read‑write splitting in Spring, providing two capabilities: (1) allowing callers to decide whether a read should use the master or a slave, and (2) ensuring that methods annotated with @Transactional always use the master for all operations.

To answer three key questions, the solution uses Spring's AbstractRoutingDataSource to route connections based on a lookup key stored in a ThreadLocal . The lookup key is set by an AOP interceptor that runs before the target method and cleared after the method finishes.

private Map
resolvedDataSources;

Both master and slave data sources are stored in this map:

resolvedDataSources.put("master", 主库数据源);
resolvedDataSources.put("salave", 从库数据源);

The abstract method determineCurrentLookupKey() returns the key that selects the appropriate DataSource:

protected abstract Object determineCurrentLookupKey();

An enum DsType distinguishes the two data source types:

package com.javacode2018.readwritesplit.base;

public enum DsType {
    MASTER, SLAVE;
}

A DsTypeHolder class holds the current DsType in a ThreadLocal :

package com.javacode2018.readwritesplit.base;

public class DsTypeHolder {
    private static ThreadLocal
dsTypeThreadLocal = new ThreadLocal<>();

    public static void master() { dsTypeThreadLocal.set(DsType.MASTER); }
    public static void slave() { dsTypeThreadLocal.set(DsType.SLAVE); }
    public static DsType getDsType() { return dsTypeThreadLocal.get(); }
    public static void clearDsType() { dsTypeThreadLocal.remove(); }
}

Any service that needs read‑write splitting implements the marker interface IService :

package com.javacode2018.readwritesplit.base;

// Services that require read‑write splitting should implement this interface
public interface IService {}

The routing data source extends AbstractRoutingDataSource and obtains the current lookup key from DsTypeHolder :

package com.javacode2018.readwritesplit.base;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

public class ReadWriteDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DsTypeHolder.getDsType();
    }
}

The ReadWriteInterceptor is an AOP aspect that runs before the target method, inspects the last argument (which may be a DsType ), sets the appropriate value in DsTypeHolder , and clears it after execution:

package com.javacode2018.readwritesplit.base;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Objects;

@Aspect
@Order(Integer.MAX_VALUE - 2) // @1
@Component
public class ReadWriteInterceptor {
    @Pointcut("target(IService)")
    public void pointcut() {}

    private Object getLastArgs(final ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        if (Objects.nonNull(args) && args.length > 0) {
            return args[args.length - 1];
        } else {
            return null;
        }
    }

    @Around("pointcut()")
    public Object around(final ProceedingJoinPoint pjp) throws Throwable {
        boolean isFirst = false;
        try {
            if (DsTypeHolder.getDsType() == null) {
                isFirst = true;
            }
            if (isFirst) {
                Object lastArgs = getLastArgs(pjp);
                if (DsType.SLAVE.equals(lastArgs)) {
                    DsTypeHolder.slave();
                } else {
                    DsTypeHolder.master();
                }
            }
            return pjp.proceed();
        } finally {
            if (isFirst) {
                DsTypeHolder.clearDsType();
            }
        }
    }
}

The configuration class registers the interceptor, enables AOP and transaction management, and defines the bean scan:

package com.javacode2018.readwritesplit.base;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableAspectJAutoProxy // @1
@EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) // @2
@ComponentScan(basePackageClasses = IService.class) // @3
public class ReadWriteConfiguration {}

The @EnableReadWrite annotation imports this configuration, allowing a user to enable the feature with a single annotation on a Spring configuration class:

package com.javacode2018.readwritesplit.base;

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ReadWriteConfiguration.class) // @1
public @interface EnableReadWrite {}

A sample Spring configuration ( MainConfig ) defines beans for the master and slave DataSource , the routing ReadWriteDataSource , a JdbcTemplate , and a transaction manager:

package com.javacode2018.readwritesplit.demo1;

import com.javacode2018.readwritesplit.base.DsType;
import com.javacode2018.readwritesplit.base.EnableReadWrite;
import com.javacode2018.readwritesplit.base.ReadWriteDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@EnableReadWrite // @1
@Configuration
@ComponentScan
public class MainConfig {
    @Bean
    public DataSource masterDs() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    @Bean
    public DataSource slaveDs() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    @Bean
    public ReadWriteDataSource dataSource() {
        ReadWriteDataSource dataSource = new ReadWriteDataSource();
        dataSource.setDefaultTargetDataSource(this.masterDs());
        Map
targetDataSources = new HashMap<>();
        targetDataSources.put(DsType.MASTER, this.masterDs());
        targetDataSources.put(DsType.SLAVE, this.slaveDs());
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

The UserService implements IService , uses JdbcTemplate for DB operations, and demonstrates how an insert runs on the master while a subsequent read can be forced to the slave via the interceptor:

package com.javacode2018.readwritesplit.demo1;

import com.javacode2018.readwritesplit.base.DsType;
import com.javacode2018.readwritesplit.base.IService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Component
public class UserService implements IService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private UserService userService; // proxy for self‑invocation

    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public String getUserNameById(long id, DsType dsType) {
        String sql = "select name from t_user where id=?";
        List
list = this.jdbcTemplate.queryForList(sql, String.class, id);
        return (list != null && !list.isEmpty()) ? list.get(0) : null;
    }

    @Transactional
    public void insert(long id, String name) {
        System.out.println(String.format("插入数据{id:%s, name:%s}", id, name));
        this.jdbcTemplate.update("insert into t_user (id,name) values (?,?)", id, name);
        String userName = this.userService.getUserNameById(id, DsType.SLAVE);
        System.out.println("查询结果:" + userName);
    }
}

A JUnit test class loads the Spring context, obtains UserService , and runs two tests: one that queries both master and slave directly, and another that inserts a record and verifies that the subsequent read (forced to slave) actually hits the master because the transaction is on the master.

package com.javacode2018.readwritesplit.demo1;

import com.javacode2018.readwritesplit.base.DsType;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo1Test {
    UserService userService;

    @Before
    public void before() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MainConfig.class);
        context.refresh();
        this.userService = context.getBean(UserService.class);
    }

    @Test
    public void test1() {
        System.out.println(this.userService.getUserNameById(1, DsType.MASTER));
        System.out.println(this.userService.getUserNameById(1, DsType.SLAVE));
    }

    @Test
    public void test2() {
        long id = System.currentTimeMillis();
        System.out.println(id);
        this.userService.insert(id, "张三");
    }
}

The SQL script creates two databases ( javacode2018_master and javacode2018_slave ) each with a t_user table and a single row to verify routing.

DROP DATABASE IF EXISTS javacode2018_master;
CREATE DATABASE IF NOT EXISTS javacode2018_master;
USE javacode2018_master;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '姓名'
);
INSERT INTO t_user (name) VALUE ('master库');

DROP DATABASE IF EXISTS javacode2018_slave;
CREATE DATABASE IF NOT EXISTS javacode2018_slave;
USE javacode2018_slave;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '姓名'
);
INSERT INTO t_user (name) VALUE ('slave库');

The full source code for the demo is available at https://gitee.com/javacode2018/spring-series under spring-series\lesson-004-readwritesplit . Readers are encouraged to star the repository.

JavatransactionAOPDatabaseSpringRead-Write SplittingAbstractRoutingDataSource
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

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.