MyBatis Plus实现根据租户进行动态数据源切换实例以及遇到的坑

本文介绍如何在SpringBoot项目中利用MyBatisPlus实现根据租户ID切换不同数据源,以解决多租户系统中数据隔离问题,避免性能相互影响。关键代码和注意事项详述,包括配置动态数据源类、数据源映射及清理机制。
摘要由CSDN通过智能技术生成

根据不同租户进行动态数据源切换实现

为什么需要根据不同租户,进行动态数据源切换?

根据不同租户数据源切换意味着根据不同租户分库。
在使用租户ID(tenantId)来区分租户场景下,多租户系统令人头疼的问题:
A租户数据量过于庞大,B租户/C租户数据量正常的情况下
A租户在操作大量数据的时候,数据库压力会直接影响到B和C租户的读写数据库的速度,导致受B和 C的租户可用性降低甚至崩溃,殃及无辜。
而分库能有效隔离租户数据,租户之间操作互不影响。

此实例基于springboot + mybatis plus实现

直接上代码~

启动配置类 DruidConfig 以及特别要注意两个坑 代码中的注释

import cn.shopex.cloud.product.handler.MyMetaObjectHander;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.stream.Stream;

/**
 * @author
 * @create
 * @desc 数据源配置类
 **/
@Configuration
public class DruidConfig {

    @Autowired
    private PaginationInterceptor pageInterceptor;
    @Bean(name = "druidDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DruidDataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dynamicDataSource() {
        return new DynamicDataSource();
    }

    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean(PaginationInterceptor paginationInterceptor) {
        MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
        // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换
        sessionFactoryBean.setDataSource(dynamicDataSource());
        sessionFactoryBean.setVfs(SpringBootVFS.class);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        //第一个坑:此项不配置 com.baomidou.mybatisplus.extension.service.IService 中的所有接口将不可用
        //字符串数组中 第一个是自己mapper.xml的位置,第二个是mybatispuls提供的默认mapper位置,
        String[] mapperLocations = new String[]{"classpath*:mybatis/*.xml",
                "classpath*:com/gitee/sunchenbin/mybatis/actable/mapping/*/*.xml"};
        Resource[] resources = Stream.of(mapperLocations)
                .flatMap(location -> Stream.of(this.getResources(resolver,location)))
                .toArray(Resource[]::new);
        sessionFactoryBean.setMapperLocations(resources);
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setLogImpl(StdOutImpl.class);
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setMetaObjectHandler(new MyMetaObjectHander());
        globalConfig.setDbConfig(new GlobalConfig.DbConfig());
        sessionFactoryBean.setGlobalConfig(globalConfig);
        sessionFactoryBean.setConfiguration(configuration);
        sessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        //第二个坑:此项不配置 mybatispuls的分页插件将不可用
        sessionFactoryBean.setPlugins(paginationInterceptor);
        return sessionFactoryBean;
    }

    /**
     * 此方法是copy源码来的
     * @param resolver
     * @param location
     * @return
     */
    private Resource[] getResources(ResourcePatternResolver resolver, String location) {
        try {
            return resolver.getResources(location);
        } catch (IOException e) {
            return new Resource[0];
        }
    }

}

动态数据源切换类 DynamicDataSource



import cn.shopex.cloud.model.product.business.EcsAdminConnection;
import cn.shopex.cloud.product.util.SpringContextUtil;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.CollectionUtils;

import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author
 * @create
 * @desc 动态数据源配置
 **/
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
     * 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    private static final ConcurrentHashMap<String, DruidDataSource> DB_RESOURCE_MAP = new ConcurrentHashMap<>();

    @Override
    public void afterPropertiesSet() {
    }

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

    /**
     * 设置数据源
     *
     * @param ecsAdminConnection
     */
    public static void setDataSource(EcsAdminConnection ecsAdminConnection) {
        Map<String, String> dataSource = ecsAdminConnection.getDataSource();
        if(CollectionUtils.isEmpty(dataSource)){
            return;
        }
        String connectionId = ecsAdminConnection.getConnectionId();
        DruidDataSource druidDataSource = DB_RESOURCE_MAP.get(connectionId);
        DruidDataSource defaultDruidDataSource = SpringContextUtil.getBean(DruidDataSource.class);
        String url = dataSource.get("product.datasource.url");
        String username = dataSource.get("product.datasource.username");
        String password = dataSource.get("product.datasource.password");
        //判断当前数据源是否存在,若不存在则创建新数据源并放入DB_RESOURCE_MAP
        if (druidDataSource == null) {
            druidDataSource = new DruidDataSource();
            BeanUtils.copyProperties(defaultDruidDataSource, druidDataSource);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            DB_RESOURCE_MAP.put(ecsAdminConnection.getConnectionId(), druidDataSource);
        } else {
            //若数据源已存在,判断当前数据源是否有改动.有改动则关闭原先数据源并更新数据源信息后放入map
            if (!username.equals(druidDataSource.getUsername()) ||
                    !password.equals(druidDataSource.getPassword()) ||
                    !url.equals(druidDataSource.getUrl())) {
                druidDataSource.close();
                druidDataSource = new DruidDataSource();
                BeanUtils.copyProperties(defaultDruidDataSource, druidDataSource);
                druidDataSource.setUrl(url);
                druidDataSource.setUsername(username);
                druidDataSource.setPassword(password);
                DB_RESOURCE_MAP.put(ecsAdminConnection.getConnectionId(), druidDataSource);
            }
        }
        CONTEXT_HOLDER.set(connectionId);
    }

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

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

    @SneakyThrows
    @Override
    protected DataSource determineTargetDataSource() {
        String connectionId = getDataSource();
        DruidDataSource druidDataSource;
        if (connectionId == null) {
            druidDataSource = SpringContextUtil.getBean(DruidDataSource.class);
        } else {
            druidDataSource = DB_RESOURCE_MAP.get(connectionId);
        }
        return druidDataSource;
    }

}


动态数据源切换类DynamicDataSource-----使用到的工具类 SpringContextUtil


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author  
 * @create  
 * @desc spring上下文util
 **/
@Component
public class SpringContextUtil implements ApplicationContextAware {
 
 
    /**
     *
     */
    private static ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }
 
    /**
     * 获取applicationContext
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
 
    /**
     * 通过name获取 Bean.
     * @param name
     * @return
     */
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }
 
    /**
     * 通过class获取Bean.
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }
 
    /**
     * 通过name,以及Clazz返回指定的Bean
     * @param name
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

动态数据源切换类DynamicDataSource-----使用到的数据源实体类 EcsAdminConnection

import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.Map;

public class EcsAdminConnection implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(
        value = "连接标识",
        notes = "连接标识"
    )
    private String connectionId;
    @ApiModelProperty(
        value = "数据源",
        notes = "数据源"
    )
    private Map<String, String> dataSource;

    public static EcsAdminConnection.EcsAdminConnectionBuilder builder() {
        return new EcsAdminConnection.EcsAdminConnectionBuilder();
    }

    public String getConnectionId() {
        return this.connectionId;
    }

    public Map<String, String> getDataSource() {
        return this.dataSource;
    }

    public void setConnectionId(String connectionId) {
        this.connectionId = connectionId;
    }

    public void setDataSource(Map<String, String> dataSource) {
        this.dataSource = dataSource;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof EcsAdminConnection)) {
            return false;
        } else {
            EcsAdminConnection other = (EcsAdminConnection)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$connectionId = this.getConnectionId();
                Object other$connectionId = other.getConnectionId();
                if (this$connectionId == null) {
                    if (other$connectionId != null) {
                        return false;
                    }
                } else if (!this$connectionId.equals(other$connectionId)) {
                    return false;
                }

                Object this$dataSource = this.getDataSource();
                Object other$dataSource = other.getDataSource();
                if (this$dataSource == null) {
                    if (other$dataSource != null) {
                        return false;
                    }
                } else if (!this$dataSource.equals(other$dataSource)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof EcsAdminConnection;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $connectionId = this.getConnectionId();
        int result = result * 59 + ($connectionId == null ? 43 : $connectionId.hashCode());
        Object $dataSource = this.getDataSource();
        result = result * 59 + ($dataSource == null ? 43 : $dataSource.hashCode());
        return result;
    }

    public String toString() {
        return "EcsAdminConnection(connectionId=" + this.getConnectionId() + ", dataSource=" + this.getDataSource() + ")";
    }

    public EcsAdminConnection(String connectionId, Map<String, String> dataSource) {
        this.connectionId = connectionId;
        this.dataSource = dataSource;
    }

    public EcsAdminConnection() {
    }

    public static class EcsAdminConnectionBuilder {
        private String connectionId;
        private Map<String, String> dataSource;

        EcsAdminConnectionBuilder() {
        }

        public EcsAdminConnection.EcsAdminConnectionBuilder connectionId(String connectionId) {
            this.connectionId = connectionId;
            return this;
        }

        public EcsAdminConnection.EcsAdminConnectionBuilder dataSource(Map<String, String> dataSource) {
            this.dataSource = dataSource;
            return this;
        }

        public EcsAdminConnection build() {
            return new EcsAdminConnection(this.connectionId, this.dataSource);
        }

        public String toString() {
            return "EcsAdminConnection.EcsAdminConnectionBuilder(connectionId=" + this.connectionId + ", dataSource=" + this.dataSource + ")";
        }
    }
}

具体调用

DynamicDataSource.setDataSource(EcsAdminConnection.builder().connectionId(connectionId).dataSource(configMap).build());

最后一个小坑:数据源 用完后clear一下。

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.RequestHandledEvent;

@Component
public class ClearUserListener implements ApplicationListener<RequestHandledEvent> {

    @Override
    public void onApplicationEvent(RequestHandledEvent event) {
        DynamicDataSource.clearDataSource();
    }

}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值