Backend Development 12 min read

Implementing Read‑Write Splitting in Spring Boot with AbstractRoutingDataSource and AOP

This article explains how to achieve read‑write splitting in a Spring Boot application by configuring multiple data sources, creating a custom AbstractRoutingDataSource, managing routing keys with ThreadLocal, and using AOP to automatically switch between master and slave databases for different CRUD operations.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Read‑Write Splitting in Spring Boot with AbstractRoutingDataSource and AOP

Read‑write splitting decides which database (master or slave) should execute a given SQL; the article chooses the programmatic approach using Spring Boot, routing data sources, and AOP.

AbstractRoutingDataSource is a Spring class that routes to a target data source based on a lookup key; the custom implementation overrides determineCurrentLookupKey() to return the key stored in DBContextHolder .

Project dependencies (excerpt from pom.xml ) include Spring Boot starter parent, spring-boot-starter-aop, spring-boot-starter-jdbc, mybatis-spring-boot-starter, mysql‑connector‑java, and commons‑lang3.

<project>
  <parent>…</parent>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
    </dependency>
    …
  </dependencies>
</project>

Data source configuration in application.yml defines one master and two slave data sources, each with its JDBC URL, username, password, and driver class.

spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://192.168.102.31:3306/test
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave1:
      jdbc-url: jdbc:mysql://192.168.102.56:3306/test
      username: pig
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave2:
      jdbc-url: jdbc:mysql://192.168.102.36:3306/test
      username: pig
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver

DataSourceConfig creates beans for the three physical data sources and a routing data source that holds them in a map and sets the master as the default.

@Configuration
public class DataSourceConfig {
    @Bean @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() { return DataSourceBuilder.create().build(); }
    @Bean @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() { return DataSourceBuilder.create().build(); }
    @Bean @ConfigurationProperties("spring.datasource.slave2")
    public DataSource slave2DataSource() { return DataSourceBuilder.create().build(); }
    @Bean
    public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource master,
                                          @Qualifier("slave1DataSource") DataSource s1,
                                          @Qualifier("slave2DataSource") DataSource s2) {
        Map
target = new HashMap<>();
        target.put(DBTypeEnum.MASTER, master);
        target.put(DBTypeEnum.SLAVE1, s1);
        target.put(DBTypeEnum.SLAVE2, s2);
        MyRoutingDataSource ds = new MyRoutingDataSource();
        ds.setDefaultTargetDataSource(master);
        ds.setTargetDataSources(target);
        return ds;
    }
}

Routing key management uses an enum DBTypeEnum (MASTER, SLAVE1, SLAVE2) and a DBContextHolder class with a ThreadLocal<DBTypeEnum> to set/get the current key, plus a round‑robin counter for slave selection.

public enum DBTypeEnum { MASTER, SLAVE1, SLAVE2 }

public class DBContextHolder {
    private static final ThreadLocal
holder = new ThreadLocal<>();
    private static final AtomicInteger counter = new AtomicInteger(-1);
    public static void set(DBTypeEnum db) { holder.set(db); }
    public static DBTypeEnum get() { return holder.get(); }
    public static void master() { set(DBTypeEnum.MASTER); }
    public static void slave() {
        int idx = counter.incrementAndGet() % 2;
        if (counter.get() > 9999) counter.set(-1);
        set(idx == 0 ? DBTypeEnum.SLAVE1 : DBTypeEnum.SLAVE2);
    }
}

MyRoutingDataSource extends AbstractRoutingDataSource and simply returns DBContextHolder.get() as the lookup key.

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

DataSourceAop defines pointcuts for read methods (select/get) and write methods (insert/add/update/delete). The @Before advice sets the routing key to slave or master accordingly.

@Aspect @Component
public class DataSourceAop {
    @Pointcut("!@annotation(com.cjs.example.annotation.Master) && (execution(* com.cjs.example.service..*.select*(..)) || execution(* com.cjs.example.service..*.get*(..)))")
    public void readPointcut() {}
    @Pointcut("@annotation(com.cjs.example.annotation.Master) || execution(* com.cjs.example.service..*.insert*(..)) || execution(* com.cjs.example.service..*.add*(..)) || execution(* com.cjs.example.service..*.update*(..)) || execution(* com.cjs.example.service..*.delete*(..))")
    public void writePointcut() {}
    @Before("readPointcut()")
    public void read() { DBContextHolder.slave(); }
    @Before("writePointcut()")
    public void write() { DBContextHolder.master(); }
}

Master annotation ( @interface Master ) can be placed on methods that must always use the master database, overriding the default read‑slave behavior.

package com.cjs.example.annotation;
public @interface Master {}

Example service MemberServiceImpl shows typical CRUD methods, a @Master ‑annotated save method, and a getToken method that forces master reads.

@Service
public class MemberServiceImpl implements MemberService {
    @Autowired private MemberMapper memberMapper;
    @Transactional public int insert(Member m) { return memberMapper.insert(m); }
    @Master @Override public int save(Member m) { return memberMapper.insert(m); }
    @Override public List
selectAll() { return memberMapper.selectByExample(new MemberExample()); }
    @Master @Override public String getToken(String appId) { return null; }
}

Test class demonstrates writing, reading (multiple reads to see round‑robin between slaves), saving with @Master , and a read‑from‑master scenario.

@RunWith(SpringRunner.class)
@SpringBootTest
public class CjsDatasourceDemoApplicationTests {
    @Autowired private MemberService memberService;
    @Test public void testWrite() { Member m = new Member(); m.setName("zhangsan"); memberService.insert(m); }
    @Test public void testRead() { for (int i=0;i<4;i++) memberService.selectAll(); }
    @Test public void testSave() { Member m = new Member(); m.setName("wangwu"); memberService.save(m); }
    @Test public void testReadFromMaster() { memberService.getToken("1234"); }
}

The article also shows the overall project structure diagram and lists several reference URLs for further reading.

JavaAOPSpring BootMyBatisRead-Write SplittingDataSource Routing
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow 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.