云计算、AI、云原生、大数据等一站式技术学习平台

网站首页 > 教程文章 正文

springboot多租户实现(1):基础架构

jxf315 2025-09-13 02:05:20 教程文章 2 ℃

基本概念

基本的多租户逻辑意思就是,业务逻辑一样,但是需要根据不同业务方动态切换不同的数据库。这样做的好处就是业务方数据物理隔离,天然分库,数据量和业务控制更加灵活。

基本交互流程

下图是我们最近实现的多租户架构,其实很简单:

1,用户登录库是同一个,登录成功返回用户所在业务方,失败返回失败信息给用户

2,网关拿到用户的业务标识后,透传给下游业务系统,业务系统链接多个datasource

3,业务系统,在filter里根据用户标识,选择datasource数据源

4,最后在操作数据库时由于数据源已经被切换,会路由到正确的数据库

技术选型

基本的架构是springboot+MySQL,动态数据源切换需要引入dynamic-datasource,进行数据源切换管理等工作

  <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.2</version>
    </dependency>

业务代码开发

第一步需要在filter里进行拦截,并切换数据源,一定是filter,不要在拦截器里写,因为filter的执行优先级大于拦截器,另外如果有多个filter的情况下,一定要先执行数据源切换的filter。

filter执行最后,记得一定要
DynamicDataSourceContextHolder.clear()下,如果有兴趣的同学可以点开看下源码,内部其实是通过ThreadLocal传递库信息

具体代码如下:

package com.ruoyi.common.core.filter;
@Order(-10000)
@Slf4j
@Component
public class DynamicSwitchDbFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
       //在DispatcherServlet之前执行
        HttpServletRequest req = (HttpServletRequest) request;
      //业务方决定对应的库
        String orgCode = req.getHeader("org_code");
        log.info(req.getRequestURI());
        if (StringUtils.isBlank(orgCode)) {
            log.error("header org code is null,can't find db");
            throw new BaseException(400, "header org code is null,can't find db");
        }
      
        log.info("switch db name:{}", orgCode);
				//切换数据源
        DynamicDataSourceContextHolder.push(orgCode);
        filterChain.doFilter(request, response);

        //通过ThreadLocal传递库信息,所以需要清楚ThreadLocal
        log.info("clean db name:{}", orgCode);
        DynamicDataSourceContextHolder.clear();
    }


    @Override
    public void destroy() {

    }
}

第二步:xml配置多数据源:下面这个配置是完整的配置,xiaomi和huawei是数据源名称,代码里通过
DynamicDataSourceContextHolder.push(“huawei”);切换数据源

spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
    dynamic:
      druid:
        initial-size: 3
        min-idle: 3
        maxActive: 10
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,slf4j
        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      datasource:
          # 主库数据源
          xiaomi:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
            username: xxx
            password: xxx
          huawei:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://xxx.xx.xxx.xxx:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
            username: xxx
            password: xxx

以上两步就可以实现数据源切换了,但实际场景比这个复杂的多,DynamicDataSource默认的数据源是master,如果找不到其它的数据源,就会使用master数据源,我们不建议使用这个master数据源。

第三步:去掉DynamicRoutingDataSource的默认数据源master,如下代码可以直接粘贴使用

package com.ruoyi.common.core.service;

/**
 * 重写DynamicRoutingDataSource,让dynamicRoutingDataSource主加载,重写目的是在找不到库名时直接报异常,不去加载主库,多租户场景没有主库的概念
 */
@Primary
@Component("dynamicRoutingDataSource")
public class RewriteDynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
    private static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
    private static final String UNDERLINE = "_";
    private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap();
    private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap();
    @Autowired
    private List<DynamicDataSourceProvider> providers;
    private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
    //多租户没有主库概念,没有找到租户对应的数据库,直接报错.
   //改动点一
    //private String primary = "master";
    private String primary = "xxxx";
    private Boolean strict = false;
    private Boolean p6spy = false;
    private Boolean seata = false;

    public RewriteDynamicRoutingDataSource(List<DynamicDataSourceProvider>  providers) {
        this.providers = providers;
    }

    protected String getPrimary() {
        return this.primary;
    }

    public DataSource determineDataSource() {
        String dsKey = DynamicDataSourceContextHolder.peek();
        return this.getDataSource(dsKey);
    }

    private DataSource determinePrimaryDataSource() {
        log.debug("dynamic-datasource switch to the primary datasource");
        DataSource dataSource = (DataSource)this.dataSourceMap.get(this.primary);
        if (dataSource != null) {
            return dataSource;
        } else {
            GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(this.primary);
            if (groupDataSource != null) {
                return groupDataSource.determineDataSource();
            } else {
                throw new CannotFindDataSourceException("dynamic-datasource can not find primary datasource");
            }
        }
    }

    public Map<String, DataSource> getDataSources() {
        return this.dataSourceMap;
    }

    public Map<String, GroupDataSource> getGroupDataSources() {
        return this.groupDataSources;
    }

    //找不到不到数据库直接报错, 改动点二
    public DataSource getDataSource(String ds) {
        if (StringUtils.isEmpty(ds)) {
            //找不到不到数据库直接报错
            throw new CannotFindDataSourceException("dynamic-datasource can not find  datasource:"+ds);
        } else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
            log.info("dynamic-datasource switch to the datasource named [{}]", ds);
            return ((GroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
        } else if (this.dataSourceMap.containsKey(ds)) {
            log.info("dynamic-datasource switch to the datasource named [{}]", ds);
            return (DataSource)this.dataSourceMap.get(ds);
        } else if (this.strict) {
            throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
        } else {
            //找不到不到数据库直接报错
            throw new CannotFindDataSourceException("dynamic-datasource can not find datasource:"+ds);
        }
    }

    public synchronized void addDataSource(String ds, DataSource dataSource) {
        DataSource oldDataSource = (DataSource)this.dataSourceMap.put(ds, dataSource);
        this.addGroupDataSource(ds, dataSource);
        if (oldDataSource != null) {
            this.closeDataSource(ds, oldDataSource);
        }

        log.info("dynamic-datasource - add a datasource named [{}] success", ds);
    }

    private void addGroupDataSource(String ds, DataSource dataSource) {
        if (ds.contains("_")) {
            String group = ds.split("_")[0];
            GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(group);
            if (groupDataSource == null) {
                try {
                    groupDataSource = new GroupDataSource(group, (DynamicDataSourceStrategy)this.strategy.getDeclaredConstructor().newInstance());
                    this.groupDataSources.put(group, groupDataSource);
                } catch (Exception var6) {
                    Exception e = var6;
                    throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
                }
            }

            groupDataSource.addDatasource(ds, dataSource);
        }

    }

    public synchronized void removeDataSource(String ds) {
        if (!StringUtils.hasText(ds)) {
            throw new RuntimeException("remove parameter could not be empty");
        } else if (this.primary.equals(ds)) {
            throw new RuntimeException("could not remove primary datasource");
        } else {
            if (this.dataSourceMap.containsKey(ds)) {
                DataSource dataSource = (DataSource)this.dataSourceMap.remove(ds);
                this.closeDataSource(ds, dataSource);
                if (ds.contains("_")) {
                    String group = ds.split("_")[0];
                    if (this.groupDataSources.containsKey(group)) {
                        DataSource oldDataSource = ((GroupDataSource)this.groupDataSources.get(group)).removeDatasource(ds);
                        if (oldDataSource == null) {
                            log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
                        }
                    }
                }

                log.info("dynamic-datasource - remove the database named [{}] success", ds);
            } else {
                log.warn("dynamic-datasource - could not find a database named [{}]", ds);
            }

        }
    }

    public void destroy() throws Exception {
        log.info("dynamic-datasource start closing ....");
        Iterator var1 = this.dataSourceMap.entrySet().iterator();

        while(var1.hasNext()) {
            Map.Entry<String, DataSource> item = (Map.Entry)var1.next();
            this.closeDataSource((String)item.getKey(), (DataSource)item.getValue());
        }

        log.info("dynamic-datasource all closed success,bye");
    }

    public void afterPropertiesSet() throws Exception {
        this.checkEnv();
        Map<String, DataSource> dataSources = new HashMap(16);
        Iterator var2 = this.providers.iterator();

        while(var2.hasNext()) {
            DynamicDataSourceProvider provider = (DynamicDataSourceProvider)var2.next();
            dataSources.putAll(provider.loadDataSources());
        }

        var2 = dataSources.entrySet().iterator();

        while(var2.hasNext()) {
            Map.Entry<String, DataSource> dsItem = (Map.Entry)var2.next();
            this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
        }

        if (this.groupDataSources.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
        } else if (this.dataSourceMap.containsKey(this.primary)) {
            log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
        } else {
            log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
        }

    }

    private void checkEnv() {
        Exception e;
        if (this.p6spy) {
            try {
                Class.forName("com.p6spy.engine.spy.P6DataSource");
                log.info("dynamic-datasource detect P6SPY plugin and enabled it");
            } catch (Exception var3) {
                e = var3;
                throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
            }
        }

        if (this.seata) {
            try {
                Class.forName("io.seata.rm.datasource.DataSourceProxy");
                log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
            } catch (Exception var2) {
                e = var2;
                throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
            }
        }

    }

    private void closeDataSource(String ds, DataSource dataSource) {
        try {
            if (dataSource instanceof ItemDataSource) {
                ((ItemDataSource)dataSource).close();
            } else {
                if (this.seata && dataSource instanceof DataSourceProxy) {
                    DataSourceProxy dataSourceProxy = (DataSourceProxy)dataSource;
                    dataSource = dataSourceProxy.getTargetDataSource();
                }

                if (this.p6spy && dataSource instanceof P6DataSource) {
                    Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
                    realDataSourceField.setAccessible(true);
                    dataSource = (DataSource)realDataSourceField.get(dataSource);
                }

                Method closeMethod = ReflectionUtils.findMethod(dataSource.getClass(), "close");
                if (closeMethod != null) {
                    closeMethod.invoke(dataSource);
                }
            }
        } catch (Exception var4) {
            Exception e = var4;
            log.warn("dynamic-datasource closed datasource named [{}] failed", ds, e);
        }

    }

    public void setStrategy(Class<? extends DynamicDataSourceStrategy> strategy) {
        this.strategy = strategy;
    }

    public void setPrimary(String primary) {
        this.primary = primary;
    }

    public void setStrict(Boolean strict) {
        this.strict = strict;
    }

    public void setP6spy(Boolean p6spy) {
        this.p6spy = p6spy;
    }

    public void setSeata(Boolean seata) {
        this.seata = seata;
    }

}


其它问题

在动态数据源DynamicRoutingDataSource实际工作中会涉及到内部调用的情况,如redis,任务调度,fegin调用,httpclient调用以及项目启动预加载数据等场景,这些场景无法确定使用哪个库,还有更复杂的多线程无法传递datasource的情况,我会持续更新相应解决方案。

最近发表
标签列表