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

}
相关推荐
武子康9 小时前
大数据-89 Spark应用必备:进程通信、序列化机制与RDD执行原理
大数据·后端·spark
shark_chili10 小时前
JITWatch实战指南:深入Java即时编译优化的黑科技工具
后端
绝无仅有10 小时前
从拉取代码到前端运行访问:Vue 前端项目的常规启动流程
后端·面试·github
小蒜学长10 小时前
spring boot驴友结伴游网站的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端
CodeLongBear10 小时前
深入理解 JVM 字节码文件:从组成结构到 Arthas 工具实践
java·jvm·后端
IT_陈寒11 小时前
SpringBoot 3.x实战:5种高并发场景下的性能优化秘籍,让你的应用快如闪电!
前端·人工智能·后端
Victor35611 小时前
Redis(47)如何配置Redis哨兵?
后端
Victor35611 小时前
Redis(46) 如何搭建Redis哨兵?
后端
尘鹄16 小时前
go 初始化组件最佳实践
后端·设计模式·golang
墩墩分墩16 小时前
【Go语言入门教程】 Go语言的起源与技术特点:从诞生到现代编程利器(一)
开发语言·后端·golang·go