springboot实现动态数据源,动态数据源原理与解析,租户系统动态数据源

18 篇文章 0 订阅
14 篇文章 0 订阅

一、springboot动态数据源原理

1. 继承AbstractRoutingDataSource得到数据源

抽象类AbstractRoutingDataSource,通过继承这个类实现根据不同的请求切换数据源。
AbstractRoutingDataSource继承自AbstractDataSource,如果声明一个类继承AbstractRoutingDataSource则这个类本身就是数据源。

2. 数据源的getConnection()方法

既然是数据源一定会用到getConnection()方法,下面看源码:

public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }

通过上面源码能分析得到数据库连接是由determineTargetDataSource()得来,下面继续分析determineTargetDataSource()方法。

3. 解析determineTargetDataSource方法

查看源码:

protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

通过这段源码能否得到数据源首先需要获取 lookupKey:
Object lookupKey = this.determineCurrentLookupKey();
然后通过这个key得到对应的数据源:
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);

4. 解析resolvedDataSources

看代码resolvedDataSources的属性,首先它是Map,通过Object可以得到DataSoure

@Nullable
private Map<Object, DataSource> resolvedDataSources;

然后这个属性的赋值代码:

public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

可以看到是将targetDataSources对象的内容赋值给了它。

5. 实现动态数据源的脉络

  1. 首先继承AbstractRoutingDataSource这个类
  2. 通过Override 这个类的方法和给这个类的属性赋值的形式,来动态的获取数据源
  3. 赋值父类的setTargetDataSources属性,将所有的数据源保存以key->value形式保存
  4. Override方法determineCurrentLookupKey(),告诉本次请求使用哪个key对应的数据源

二、实现springboot动态数据源代码主要的三个类

1. DynamicDataSource类

这个类Override父级的几个方法,和赋值父类的属性来实现动态数据源

package com.example.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.example.holder.TenantHolder;
import com.example.pojo.Tenant;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author wangy
 * @create 2021/7/12 17:18
 * @Description 集成AbstractRoutingDataSource实现动态数据源
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    //默认数据源
    private static DataSource defaultDataSource;
    //保存所有的数据源
    private static Map<Object,Object> targetDataSources = new HashMap<>();

    static {
        //初始化默认数据源
        DruidDataSource source = new DruidDataSource();
        source.setUrl("jdbc:mysql://localhost/defaultDB?useUnicode=true&characterEncoding=UTF-8");
        source.setUsername("root");
        source.setPassword("123456");
        source.setDriverClassName("com.mysql.cj.jdbc.Driver");
        source.setInitialSize(2);
        source.setMinIdle(2);
        source.setMaxActive(5);
        defaultDataSource = source;
    }

    /**
     * 获取数据库连接
     * @return
     * @throws SQLException
     */
    @Override
    public Connection getConnection() throws SQLException {
        return super.getConnection();
    }

    /**
     * 设置默认数据源
     * @param defaultTargetDataSource 存储所有租户数据源信息的数据源
     */
    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    @Override
    public void afterPropertiesSet() {
        //初始化所有租户的数据源
        initTargetDataSources();
        //一些参数设置操作
        super.afterPropertiesSet();
    }

    /**
     * 初始化所有租户的数据源
     */
    public void initTargetDataSources(){
        //得到所有动态数据源
        JdbcTemplate jdbcTemplate = new JdbcTemplate(defaultDataSource);
        List<Map<String, Object>> list =
                jdbcTemplate.queryForList("select tenant_name, tenant_code, db_url, db_user, db_password, tenant_introduce from t_tenant;");

        Tenant tenant = null;
        for (Map<String, Object> map : list) {
            tenant = new Tenant();
            tenant.setTenantName((String) map.get("tenant_name"));
            tenant.setTenantCode((String) map.get("tenant_code"));
            tenant.setDbUrl((String) map.get("db_url"));
            tenant.setDbUser((String) map.get("db_user"));
            tenant.setDbPassword((String) map.get("db_password"));
            tenant.setTenantIntroduce((String) map.get("tenant_introduce"));
            //创建租户的数据源
            DataSource tenantDataSource = createDataSourceByTTenant(tenant);
            //放到容器中
            targetDataSources.put(tenant.getTenantCode(), tenantDataSource);
        }
        //设置所有的数据源Map
        super.setTargetDataSources(targetDataSources);
    }

    /**
     * 每次sql请求会获取使用哪个数据源对应的key
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantHolder.getTenantCode();
    }

    /**
     * 每次sql请求会决定使用哪个数据源
     * @return
     */
    @Override
    protected DataSource determineTargetDataSource() {

        DataSource dataSource = null;
        //如果未获取到
        if (null == determineCurrentLookupKey()) {
            dataSource =  defaultDataSource;
        }else {
            dataSource = (DataSource) targetDataSources.get(determineCurrentLookupKey());
        }
        return dataSource;
    }

    /**
     * 根据表里数据源 初始化话数据源
     * @param tenant
     * @return
     */
    public DataSource createDataSourceByTTenant(Tenant tenant){
        DruidDataSource source = new DruidDataSource();
        source.setUrl(tenant.getDbUrl());
        source.setUsername(tenant.getDbUser());
        source.setPassword(tenant.getDbPassword());
        source.setDriverClassName("com.mysql.cj.jdbc.Driver");
        source.setInitialSize(2);
        source.setMinIdle(1);
        source.setMaxActive(3);
        return source;
    }

}

2. TenantHolder类

每次请求都会是一个线程,怎么得到当前线程是哪个租户请求的,通过ThreadLocal来保存租户请求的标识

public class TenantHolder {

    //通过拦截器字符串截取 host.split("\\.")[0] 来保存当前租户的标识
    private static ThreadLocal<String> tenantCode = new ThreadLocal<>();

    public static String getTenantCode() {
        return tenantCode.get();
    }

    public static void setTenantCode(String code) {
        tenantCode.set(code);
    }

    public static void remove(){
        tenantCode.remove();
    }
}

3. 实现Filter类

这个类作用是截取各个租户独有的域名标识,然后保存到当前线程的TenantHolder类属性中,获取动态数据源的时候即可通过Override方法determineCurrentLookupKey()返回这个标识从而得到对应租户的数据源。

@Component
@WebFilter(urlPatterns = "/**")
public class TenantHolderFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(TenantHolderFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        log.info("+++++++++++++请求路径:[=>{}<=]+++请求时间:[{}]++++++++++++++++++++++++", request.getRequestURL(), LocalDateTime.now());
        String host = request.getHeader("Host");
        log.info("处理请求的线程是:[{}],host是:[{}]", Thread.currentThread().getName(), host);
        String tenantCode = host.split("\\.")[0];
        //设置tenant
        TenantHolder.setTenantCode(tenantCode);
        try {
            chain.doFilter(request, response);
        } catch (Exception e) {
            //异常处理
            log.info("{}", e.getMessage());
        } finally {
            //移除tenant
            TenantHolder.remove();
        }
    }
}

三、实现效果演示

请求接口:http://zhangsan.localhost:8081/system/getSystemInfo
返回:
{
“id”: 1,
“keyName”: “张三的独立数据库”,
“keyValue”: “这是张三租户系统的数据源”,
“addTime”: “2021-07-12T15:58:14”,
“updateTime”: “2021-07-12T15:58:16”,
“deleted”: false
}

请求接口:http://lisi.localhost:8081/system/getSystemInfo
返回:
{
“id”: 1,
“keyName”: “这是李四的独立数据库”,
“keyValue”: “这是李四的租户系统的数据源”,
“addTime”: “2021-07-16T14:40:16”,
“updateTime”: “2021-07-16T14:40:19”,
“deleted”: false
}

四、本项目源码参考

1.本项目源码

说一千道一万,有个源码最划算,下面试本项目的源码,欢迎大家指正。
https://gitee.com/wangyue123com_admin/dynamic-datasource.git

2.参考

其他参考链接:
https://www.jianshu.com/p/edc282e6bbb9
某位前辈的视频原理解析:
https://www.bilibili.com/video/BV13y4y1g7zU?from=search&seid=5514249341295337338

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wangyue23com

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

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

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

打赏作者

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

抵扣说明:

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

余额充值