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

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

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

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

  **根据不同租户数据源切换意味着根据不同租户分库。

    在使用租户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 CONTEXT_HOLDER = new ThreadLocal<>();

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

    @Override

    public void afterPropertiesSet() {

    }

    @Override

    protected Object determineCurrentLookupKey() {

        return getDataSource();

    }

    /**

    * 设置数据源

    *

    * @param ecsAdminConnection

    */

    public static void setDataSource(EcsAdminConnection ecsAdminConnection) {

        Map 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

    * @return

    */

    public static T getBean(Class clazz){

        return getApplicationContext().getBean(clazz);

    }

    /**

    * 通过name,以及Clazz返回指定的Bean

    * @param name

    * @param clazz

    * @param

    * @return

    */

    public static T getBean(String name,Class 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 dataSource;

    public static EcsAdminConnection.EcsAdminConnectionBuilder builder() {

        return new EcsAdminConnection.EcsAdminConnectionBuilder();

    }

    public String getConnectionId() {

        return this.connectionId;

    }

    public Map getDataSource() {

        return this.dataSource;

    }

    public void setConnectionId(String connectionId) {

        this.connectionId = connectionId;

    }

    public void setDataSource(Map 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 dataSource) {

        this.connectionId = connectionId;

        this.dataSource = dataSource;

    }

    public EcsAdminConnection() {

    }

    public static class EcsAdminConnectionBuilder {

        private String connectionId;

        private Map dataSource;

        EcsAdminConnectionBuilder() {

        }

        public EcsAdminConnection.EcsAdminConnectionBuilder connectionId(String connectionId) {

            this.connectionId = connectionId;

            return this;

        }

        public EcsAdminConnection.EcsAdminConnectionBuilder dataSource(Map 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 {

    @Override

    public void onApplicationEvent(RequestHandledEvent event) {

        DynamicDataSource.clearDataSource();

    }

}

```

你可能感兴趣的:(MyBatis Plus实现根据租户进行动态数据源切换实例以及遇到的坑)