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.
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.DriverDataSourceConfig 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.