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<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();

    }

}

```

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351