Master Dynamic Data Source Switching with ThreadLocal in SpringBoot
This guide explains how to implement dynamic data source switching in SpringBoot by leveraging ThreadLocal and AbstractRoutingDataSource, covering basic concepts, code examples for context holder, routing datasource, configuration, annotation‑driven switching, and runtime addition of new data sources with practical testing steps.
1 Introduction
The article introduces ThreadLocal and AbstractRoutingDataSource as the foundation for dynamic data source switching in SpringBoot.
ThreadLocal
ThreadLocal (thread local variable) provides each thread with its own copy of a variable, ensuring isolation between threads and reducing synchronization overhead.
Purpose: share data within a single thread while keeping different threads isolated.
Mechanism: stores the value in a Map inside the thread instance using the thread as the key.
AbstractRoutingDataSource
AbstractRoutingDataSource selects the current data source based on a user‑defined rule. It invokes the abstract method determineCurrentLookupKey() before each query to decide which data source to use.
2 Code Implementation
Program environment: SpringBoot 2.4.8 Mybatis‑plus 3.2.0 Druid 1.2.6 lombok 1.18.20 commons‑lang3 3.10
2.1 Implement ThreadLocal
Create a holder class to manage the data source name via set, get, and remove methods.
/**
* @author: jiangjs
* @description:
* @date: 2023/7/27 11:21
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* Set data source name
*/
public static void setDataSource(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* Get current thread's data source name
*/
public static String getDataSource(){
return DATASOURCE_HOLDER.get();
}
/**
* Remove current data source
*/
public static void removeDataSource(){
DATASOURCE_HOLDER.remove();
}
}2.2 Implement AbstractRoutingDataSource
Define a dynamic data source class that extends AbstractRoutingDataSource and links the lookup key to the ThreadLocal holder.
/**
* @author: jiangjs
* @description: Implement dynamic data source routing
* @date: 2023/7/27 11:18
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}2.3 Configure Database
Configure master and slave data sources in application.yml and expose them via a configuration class.
# Set data sources
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 15
min-idle: 15
max-active: 200
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: ""
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
connection-properties: false
/**
* @author: jiangjs
* @description: DataSource configuration
* @date: 2023/7/27 11:34
*/
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource(){
Map<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master", defaultDataSource);
dataSourceMap.put("slave", slaveDataSource());
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}2.4 Test
Create a simple table in both master and slave databases:
create table test_user(
user_name varchar(255) not null comment '用户名'
);Insert distinct records:
insert into test_user (user_name) value ('master');
insert into test_user (user_name) value ('slave');Expose an endpoint that switches the data source based on a path variable:
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
DataSourceContextHolder.setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return testUser.getUserName();
}Result screenshots:
2.5 Optimization Adjustments
2.5.1 Annotation‑Driven Switching
Define a custom annotation @DS to specify the target data source.
/**
* @author: jiangjs
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
}Implement an AOP aspect that sets and clears the data source around the annotated method.
@Aspect
@Component
@Slf4j
public class DSAspect {
@Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)")
public void dynamicDataSource(){}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)) {
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}Test endpoints:
@GetMapping("/getMasterData.do")
public String getMasterData(){
TestUser testUser = testUserMapper.selectOne(null);
return testUser.getUserName();
}
@GetMapping("/getSlaveData.do")
@DS("slave")
public String getSlaveData(){
TestUser testUser = testUserMapper.selectOne(null);
return testUser.getUserName();
}Result screenshots:
2.5.2 Dynamic Addition of Data Sources
Entity representing a data source:
/**
* @author: jiangjs
*/
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
private String key;
}Enhanced DynamicDataSource with a method to create data sources at runtime.
/**
* @author: jiangjs
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map<Object,Object> targetDataSourceMap;
public DynamicDataSource(DataSource defaultDataSource, Map<Object,Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
public void createDataSource(List<DataSourceEntity> dataSources){
try {
if (CollectionUtils.isNotEmpty(dataSources)) {
for (DataSourceEntity ds : dataSources) {
Class.forName(ds.getDriverClassName());
DriverManager.getConnection(ds.getUrl(), ds.getUserName(), ds.getPassWord());
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(ds, dataSource);
dataSource.setTestOnBorrow(true);
dataSource.setTestWhileIdle(true);
dataSource.setValidationQuery("select 1 ");
dataSource.init();
this.targetDataSourceMap.put(ds.getKey(), dataSource);
}
super.setTargetDataSources(this.targetDataSourceMap);
super.afterPropertiesSet();
}
} catch (ClassNotFoundException | SQLException e) {
log.error("---程序报错---:{}", e.getMessage());
}
}
public boolean existsDataSource(String key){
return Objects.nonNull(this.targetDataSourceMap.get(key));
}
}Runner that loads data source definitions from a database table at application startup:
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbInfoMapper testDbInfoMapper;
@Override
public void run(String... args) throws Exception {
List<TestDbInfo> testDbInfos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDbInfos)) {
List<DataSourceEntity> ds = new ArrayList<>();
for (TestDbInfo testDbInfo : testDbInfos) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDbInfo, sourceEntity);
sourceEntity.setKey(testDbInfo.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}SQL for storing data source metadata:
create table test_db_info(
id int auto_increment primary key not null comment '主键Id',
url varchar(255) not null comment '数据库URL',
username varchar(255) not null comment '用户名',
password varchar(255) not null comment '密码',
driver_class_name varchar(255) not null comment '数据库驱动',
name varchar(255) not null comment '数据库名称'
);Insert a slave data source record as an example:
insert into test_db_info(url, username, password, driver_class_name, name)
value ('jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false',
'root', '123456', 'com.mysql.cj.jdbc.Driver', 'add_slave');After application startup, the new data source becomes available for the same test endpoints, confirming dynamic addition works.
Conclusion: By combining ThreadLocal, AbstractRoutingDataSource, custom annotations, and runtime data source registration, developers can achieve flexible and scalable multi‑database access in SpringBoot applications.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
