Read‑Write Splitting with Spring Boot, MyBatis and a Custom Routing DataSource
This article demonstrates how to implement database read‑write splitting in a Spring Boot application by configuring multiple MySQL data sources, creating a custom AbstractRoutingDataSource, using ThreadLocal routing keys, and applying AOP to switch between master and slave databases for CRUD operations.
The article introduces the concept of read‑write splitting, explaining that the application can decide which database (master or slave) to use for each SQL statement, either via middleware or directly in the application layer.
It focuses on the Spring‑based implementation using AbstractRoutingDataSource to route connections based on a lookup key.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
...
</dependencies>
</project>The application.yml defines three physical data sources (master, slave1, slave2) with their JDBC URLs, usernames, passwords and driver class names.
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.DriverJava configuration creates a DataSource bean for each physical source and registers them in a custom MyRoutingDataSource which extends AbstractRoutingDataSource . The routing datasource holds a map of target data sources keyed by an enum.
@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;
}
}MyBatis is configured to use the routing datasource and to load mapper XML files.
@Configuration
@EnableTransactionManagement
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(myRoutingDataSource);
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return factory.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}A DBTypeEnum defines the three logical databases, and DBContextHolder stores the current lookup key in a ThreadLocal together with a simple round‑robin counter for slave selection.
public enum DBTypeEnum { MASTER, SLAVE1, SLAVE2 }
public class DBContextHolder {
private static final ThreadLocal
contextHolder = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
public static void set(DBTypeEnum db) { contextHolder.set(db); }
public static DBTypeEnum get() { return contextHolder.get(); }
public static void master() { set(DBTypeEnum.MASTER); }
public static void slave() {
int idx = counter.getAndIncrement() % 2;
if (counter.get() > 9999) counter.set(-1);
set(idx == 0 ? DBTypeEnum.SLAVE1 : DBTypeEnum.SLAVE2);
}
}The routing datasource overrides determineCurrentLookupKey() to return the value from DBContextHolder .
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}An AOP aspect defines pointcuts for read‑only methods (select, get, find) and write methods (insert, update, delete, etc.). Before each method the aspect sets the appropriate routing key via DBContextHolder.master() or DBContextHolder.slave() . A custom @Master annotation can force master usage.
@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..*.update*(..)) || execution(* com.cjs.example.service..*.delete*(..))")
public void writePointcut() {}
@Before("readPointcut()")
public void read() { DBContextHolder.slave(); }
@Before("writePointcut()")
public void write() { DBContextHolder.master(); }
}An example MemberService shows a normal read method, a write method annotated with @Master , and how the routing works in practice.
@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); }
public List
selectAll() { return memberMapper.selectByExample(new MemberExample()); }
@Master @Override
public String getToken(String appId) { return null; }
}JUnit tests create members, invoke write and read methods, and verify that the routing prints messages such as "切换到master" or "切换到slave1" in the console.
@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 testReadFromMaster() { memberService.getToken("1234"); }
}The article concludes with a project directory diagram and a list of reference links for further reading.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
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.