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";
    }

}
相关推荐
一个做软件开发的牛马15 分钟前
Spring Boot 自动配置原理揭秘:从 @SpringBootApplication 到手写自定义 Starter
java·后端
周杰伦fans28 分钟前
续集:工作空间一切换,我的插件菜单就消失?——MenuBar与Ribbon的自动重载方案
后端·ribbon·c#
可乐ea1 小时前
【Spring Boot + MyBatis|第7篇】JWT 登录认证与拦截器实现
java·spring boot·后端·mybatis·状态模式
西安邮电大学1 小时前
有关栈的经典算法题
java·后端·其他·算法·面试
摇滚侠2 小时前
SpringMVC 入门到实战 配置类替换 XML 配置文件 86-91
xml·java·后端·spring·maven·intellij-idea
我登哥MVP2 小时前
SpringCloud Alibaba 核心组件解析:服务注册与发现(Nacos)
java·spring boot·后端·spring·spring cloud·java-ee·maven
摇滚侠2 小时前
SpringMVC 入门到实战 处理静态资源的过程 64
java·后端·spring·maven·intellij-idea
摇滚侠2 小时前
MyBatis 入门到项目实战 MyBatis 核心配置文件 15-19
java·tomcat·mybatis
Liquad Li2 小时前
ABP vNext 标准分层解决方案项目结构完整解析
后端
布朗克1683 小时前
39 Spring Boot Web实战
前端·spring boot·后端·实战