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.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Master Dynamic Data Source Switching with ThreadLocal in SpringBoot

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.

JavaDynamic Data SourceSpringBootMyBatis-PlusThreadLocal
Java High-Performance Architecture
Written by

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.

0 followers
Reader feedback

How this landed with the community

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.