SpringBoot 实现DataSource接口实现多租户数据源切换方案

摘要:本文主要介绍在多租户环境下的数据源切换方案,该方案和springboot提供的方案类似,但是这种方案更适合动态扩展,且在springboot视角下永远只有一个DataSource,在实际获取connection的时候才进行动态更换数据源,更简单。

实现

案例基于Springboot3的版本开发,也兼容其他springboot版本。

pom.xml

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    </dependency>
</dependencies>

application.yml配置

yaml 复制代码
multi:
  datasource:
    tenant1:
      tenant-id: 1
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://192.168.8.134:30635/test_1?useSSL=false&serverTimezone=UTC
      username: super_admin
      password: super_admin
    tenant2:
      tenant-id: 2
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://192.168.8.134:30635/test_2?useSSL=false&serverTimezone=UTC
      username: super_admin
      password: super_admin

MultiDataSourceProperties

简单的属性配置类

arduino 复制代码
@Data
public class MultiDataSourceProperties {

    /** 租户id */
    private String tenantId;

    /** 数据库驱动名称 */
    private String driverClassName;

    /**  */
    private String jdbcUrl;

    /**  */
    private String username;

    /**  */
    private String password;

}

MultiDataSource实现的数据源接口

其实就是做了一层代理,让获取的数据源根据上下文动态切换。

java 复制代码
public class MultiDataSource implements DataSource {

    private final MultiDataSourceSelector multiDatasourceSelector;

    public MultiDataSource(MultiDataSourceSelector multiDatasourceSelector) {
        this.multiDatasourceSelector = multiDatasourceSelector;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return multiDatasourceSelector.getCurrentDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return multiDatasourceSelector.getCurrentDataSource().getConnection(username, password);
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return multiDatasourceSelector.getCurrentDataSource().getLogWriter();
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        multiDatasourceSelector.getCurrentDataSource().setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        multiDatasourceSelector.getCurrentDataSource().setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return multiDatasourceSelector.getCurrentDataSource().getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return multiDatasourceSelector.getCurrentDataSource().getParentLogger();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return multiDatasourceSelector.getCurrentDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return multiDatasourceSelector.getCurrentDataSource().isWrapperFor(iface);
    }
}

MultiDataSourceConfig配置

这里只需要自己定义一个DataSource即可,其他的都交给Spring管理

typescript 复制代码
@Configuration
public class MultiDataSourceConfig {

    @ConfigurationProperties(prefix = "multi.datasource.tenant1")
    @Bean
    public MultiDataSourceProperties multiDataSourceProperties1(){
        return new MultiDataSourceProperties();
    }

    @ConfigurationProperties(prefix = "multi.datasource.tenant2")
    @Bean
    public MultiDataSourceProperties multiDataSourceProperties2(){
        return new MultiDataSourceProperties();
    }

    @Bean
    public DataSource dataSource(List<MultiDataSourceProperties> multiDataSourceProperties){
        return new MultiDataSource(new MultiDataSourceSelector(multiDataSourceProperties));
    }

}

MultiDataSourceSelector

实际的动态数据源构造类,这里可以做成动态的,我只是给了静态的案例。

scss 复制代码
public class MultiDataSourceSelector {

    /**
     * 数据源配置列表
     */
    private List<MultiDataSourceProperties> multiDataSourcePropertiesList;

    private final Map<String, HikariDataSource> DATA_SOURCE_MAP = new ConcurrentHashMap<>();

    public MultiDataSourceSelector(List<MultiDataSourceProperties> multiDataSourcePropertiesList) {
        this.multiDataSourcePropertiesList = multiDataSourcePropertiesList;
    }

    public DataSource getCurrentDataSource(){
        String tenantId = MultiTenantContextHolder.getTenantId();
        if(DATA_SOURCE_MAP.containsKey(tenantId)){
            return DATA_SOURCE_MAP.get(tenantId);
        }
        // TODO 这里如果是动态数据源,就可以写为调用接口的方式,
        return DATA_SOURCE_MAP.computeIfAbsent(tenantId, (key) -> {
            MultiDataSourceProperties multiDataSourceProperties = multiDataSourcePropertiesList.stream().filter(v -> v.getTenantId().equals(tenantId)).findFirst().orElse(null);
            if(null == multiDataSourceProperties){
                throw new RuntimeException("not support tenant : " + tenantId);
            }
            return new HikariDataSource(buildHikariConfig(multiDataSourceProperties));
        });
    }

    /**
     * 构建hikari 连接配置信息
     * @param properties
     * @return
     */
    private HikariConfig buildHikariConfig(MultiDataSourceProperties properties){
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(properties.getDriverClassName());
        hikariConfig.setJdbcUrl(properties.getJdbcUrl());
        hikariConfig.setUsername(properties.getUsername());
        hikariConfig.setPassword(properties.getPassword());
        hikariConfig.setPoolName("tenant-" + properties.getTenantId());
        return hikariConfig;
    }

}

MultiTenantContextHolder

租户上下文类,用于切换租户用,建议切换为阿里的线程池。

typescript 复制代码
public class MultiTenantContextHolder {

    public static final ThreadLocal<String> TENANT_ID_CONTEXT = new ThreadLocal<>();

    public static void setTenantId(String tenantId){
        TENANT_ID_CONTEXT.set(tenantId);
    }

    public static String getTenantId(){
        return TENANT_ID_CONTEXT.get();
    }

    public static void clearTenantId(){
        TENANT_ID_CONTEXT.remove();
    }

}

测试案例

TestEntity

less 复制代码
@Data
@TableName("test")
public class TestEntity {

    private Long id;

    private String name;

    private Date createTime;

}

TestMapper

java 复制代码
@Mapper
public interface TestMapper extends BaseMapper<TestEntity> {

}

TestService

ini 复制代码
@Service
public class TestService {

    @Autowired
    private TestMapper testMapper;

    @Transactional
    public void tranSave(){
        for (int i = 0; i < 3; i++) {
            TestEntity testEntity = new TestEntity();
            testEntity.setId(IdWorker.getId());
            testEntity.setName(Thread.currentThread().getName() + "循环次数" + i);
            testEntity.setCreateTime(new Date());
            testMapper.insert(testEntity);
            if(i==2){
                int a= 1/0;
            }
        }
    }

    public void noSave(){
        for (int i = 0; i < 3; i++) {
            TestEntity testEntity = new TestEntity();
            testEntity.setId(IdWorker.getId());
            testEntity.setName(Thread.currentThread().getName() + "循环次数" + i);
            testEntity.setCreateTime(new Date());
            testMapper.insert(testEntity);
            if(i==2){
                int a= 1/0;
            }
        }
    }

}

TestController

租户上下文的设置,建议写在filter上,下面只是做案例

typescript 复制代码
@Slf4j
@RestController
@RequestMapping(value = "test")
public class TestController {

    @Autowired
    private TestMapper testMapper;
    @Autowired
    private TestService testService;

    @GetMapping("kua")
    public Object kua(){
        MultiTenantContextHolder.setTenantId("1");
        TestEntity testEntity = testMapper.selectById(1);
        log.info("查询的数据={}",testEntity);

        Thread thread = new Thread(() -> {
            MultiTenantContextHolder.setTenantId("2");
            TestEntity testEntity2 = testMapper.selectById(1);
            log.info("查询的数据2={}",testEntity2);
        });
        thread.start();
        testEntity = testMapper.selectById(1);
        log.info("查询的数据1={}",testEntity);
        return "success";
    }

    @GetMapping("abc")
    public Object abc(String tenantId){
        MultiTenantContextHolder.setTenantId(tenantId);
        TestEntity testEntity = testMapper.selectById(1);
        log.info("查询的数据={}",testEntity);
        return "success";
    }

    @GetMapping("trans")
    public Object trans(String tenantId){
        MultiTenantContextHolder.setTenantId(tenantId);
        testService.tranSave();
        return "success";
    }

    @GetMapping("noSave")
    public Object noSave(String tenantId){
        MultiTenantContextHolder.setTenantId(tenantId);
        testService.noSave();
        return "success";
    }

}
相关推荐
shark_chili4 分钟前
面试官再问synchronized底层原理,这样回答让他眼前一亮!
后端
灵魂猎手26 分钟前
2. MyBatis 参数处理机制:从 execute 方法到参数流转全解析
java·后端·源码
易元32 分钟前
模式组合应用-桥接模式(一)
后端·设计模式
柑木36 分钟前
隐私计算-SecretFlow/SCQL-SCQL的两种部署模式
后端·安全·数据分析
灵魂猎手37 分钟前
1. Mybatis Mapper动态代理创建&实现
java·后端·源码
泉城老铁38 分钟前
在秒杀场景中,如何通过动态调整线程池参数来应对流量突增
后端·架构
小悲伤39 分钟前
金蝶eas-dep反写上游单据
后端
用户91942877459540 分钟前
FastAPI (Python 3.11) Linux 实战搭建与云部署完全指南(经验)
后端
板板正1 小时前
Spring Boot 整合MongoDB
spring boot·后端·mongodb
bobz9652 小时前
恶补 vhost,vDPA
后端