动态数据源的简单使用

1、前言

当我们需要搭建一个平台给不同的客户群体使用同一套应用时,又需要针对不同的客户群里数据隔离,这就迎来了多租户的时代。

多租户的数据库级别的隔离就需要根据不同租户配置不同的数据源。

Mybatis-plus中提供了动态数据源dynamic-datasource-spring-boot-starter 来实现,详细的使用文档确实收费的,有兴趣的可以付费学习一下。

今天自己实现动态数据源的切换以及新增。

2、关键信息

动态数据源的切换会使用到ThreadLocalAbstractRoutingDataSource

  • ThreadLocal:主要实现不同线程的资源隔离,用来储存当前线上的数据源标识。
  • AbstractRoutingDataSource: 根据用户定义的规则选择当前的数据源,需要重写抽象方法determineCurrentLookupKey(),决定使用哪一个数据源。

3、环境准备

  • SpringBoot 2.6.13
  • mybatis-plus 3.5.7
  • lombok1.18.24

4、实现ThreadLocal

创建本地线程类,用来保存当前线程的需要使用的数据源简称或者标识。

/**
 *
 *
 * @author: ws
 * @date: 2024/9/18 10:09
 */
public class DataSourceContextHolder {

    private static final ThreadLocal<String> DATASOURCE_CONTEXT = new ThreadLocal<>();

    private  DataSourceContextHolder(){

    }

    public static void setDataSource(String dataSourceName){
        DATASOURCE_CONTEXT.set(dataSourceName);
    }

    public static String getDataSource(){
        return DATASOURCE_CONTEXT.get();
    }

    public static void clearDataSource(){
        DATASOURCE_CONTEXT.remove();
    }
}

5、实现AbstractRoutingDataSource

类的全路径:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource

定义一个动态加载数据源的类,通过determineCurrentLookupKey()判断最终使用哪个数据源

/**
 * 动态数据源
 *
 * @author: ws
 * @date: 2024/9/6 13:08
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    private final Map<Object, Object> targetDataSources = new HashMap<>();

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }

    /**
     * 加载所有的数据源
     *
     * @author ws
     * @date 2024/9/18 15:35
     */
    public void createDateSource(List<DataSourceEntity> entityList){
        try{
            if (CollectionUtils.isNotEmpty(entityList)) {
                for (DataSourceEntity entity : entityList) {
                    // 校验数据库连接是否有效
                    log.info("校验数据库连接是否有效>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
                    Class.forName(entity.getDriverClassName());
                    Connection connection = DriverManager.getConnection(entity.getUrl(), entity.getUsername(), entity.getPassword());
                    if (connection != null) {
                        HikariDataSource dataSource = getHikariDataSource(entity);
                        targetDataSources.put(entity.getKey(), dataSource);
                    }

                    super.setTargetDataSources(targetDataSources);
                    //将TargetDataSources中的连接信息放入resolvedDataSources管理
                    super.afterPropertiesSet();
                }
            }
        }catch (Exception e){
            log.error("检查数据源异常:", e);
        }
    }

    public HikariDataSource getHikariDataSource(DataSourceEntity entity) {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(entity.getDriverClassName());
        hikariConfig.setJdbcUrl(entity.getUrl());
        hikariConfig.setUsername(entity.getUsername());
        hikariConfig.setPassword(entity.getPassword());

        return new HikariDataSource(hikariConfig);
    }

    /**
     * 动态新增数据源
     *
     * @author ws
     * @date 2024/9/18 15:39
     */
    public void addDateSource(DataSourceEntity entity){
        try {
            log.info("校验数据库连接是否有效>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
            Class.forName(entity.getDriverClassName());
            Connection connection = DriverManager.getConnection(entity.getUrl(), entity.getUsername(), entity.getPassword());
            if (connection != null) {
                // 关闭老的数据库链接
                Map<Object, DataSource> resolvedDataSources = getResolvedDataSources();
                if (CollectionUtils.isNotEmpty(resolvedDataSources)) {
                    resolvedDataSources.forEach((k, v) -> {
                        // 因为这里使用的是HikariDataSource数据源,所以可以直接强转
                        HikariDataSource  hikariDataSource = (HikariDataSource) v;
                        hikariDataSource.close();
                    });
                }

                HikariDataSource dataSource = getHikariDataSource(entity);
                targetDataSources.put(entity.getKey(), dataSource);
            }

            super.setTargetDataSources(targetDataSources);
            //将TargetDataSources中的连接信息放入resolvedDataSources管理
            super.afterPropertiesSet();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

其中createDateSource() 用来加来所有的数据源,博文中使用的数据源是HikariDataSource
addDateSource()是动态的新增一个新的数据源,并重新初始化所有的数据源信息。

determineCurrentLookupKey() 直接从DataSourceContextHolder 中获取当前数据源的简称。

6、数据源信息实体DataSourceEntity

DataSourceEntity 记录数据源的链接信息以及别名简称

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DataSourceEntity implements Serializable {

    private static final long serialVersionUID = 1584256444573613974L;

    /**
     * 数据源的Key
     */
    private String key;

    /**
     * 数据库连接地址
     */
    private String url;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 驱动
     */
    private String driverClassName;
}

7、数据源的配置

Spring容器启动的时候加载一个数据源

@Configuration
public class DataSourceConfig {

    @Bean
    public DynamicDataSource dynamicDataSource(){
        DynamicDataSource dynamicDataSource = new DynamicDataSource();

        List<DataSourceEntity> dsList = new ArrayList<>();
        dsList.add(new DataSourceEntity("dev", "jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8", "root", "root", "com.mysql.cj.jdbc.Driver"));
        dynamicDataSource.createDateSource(dsList);
        return dynamicDataSource;
    }
}

8、测试控制层

其中类似AuctionPlanMapper 使用的Mybatis-plus,可以自行生成。devtest笔者在测试的时候分别代表的不同的开发测试库和测试数据库。

/**
 * TODO
 *
 * @author: ws
 * @date: 2024/9/18 10:20
 */
@RestController
public class TestController {

    @Resource
    private AuctionPlanMapper auctionPlanMapper;
    @Resource
    private DynamicDataSource dynamicDataSource;


    @GetMapping("/testDev")
    public Object testDev(){
        DataSourceContextHolder.setDataSource("dev");
        AuctionPlanExt auctionPlanExt = auctionPlanMapper.selectById(3);
        DataSourceContextHolder.clearDataSource();
        return auctionPlanExt;
    }

    @GetMapping("/testInt")
    public Object testInt(){
        DataSourceContextHolder.setDataSource("test");
        AuctionPlanExt auctionPlanExt = auctionPlanMapper.selectById(3);
        DataSourceContextHolder.clearDataSource();
        return auctionPlanExt;
    }

    @GetMapping("/addDataSource")
    public String addDataSource(){
        DataSourceEntity dataSourceEntity = new DataSourceEntity("test", "jdbc:mysql://127.0.0.1:3306/test02?characterEncoding=utf-8", "root", "root", "com.mysql.cj.jdbc.Driver");
        dynamicDataSource.addDateSource(dataSourceEntity);
        return "success";
    }
}

9、项目测试

在这里插入图片描述因为配置中只加载了一个数据源,所以只检测了一个数据源。

9.1 测试/testDev

在这里插入图片描述

9.2 测试/testInt

在这里插入图片描述报错了,因为笔者没有配置test的地址。

9.3 测试/addDataSource

在这里插入图片描述从日志可以看出,加载了两个数据源。

9.4 重新测试/testInt

在这里插入图片描述
结果有了。返回单额数据和dev的数据不一致,确实是两个库的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值