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
private static final ConcurrentHashMap
@Override
public void afterPropertiesSet() {
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
/**
* 设置数据源
*
* @param ecsAdminConnection
*/
public static void setDataSource(EcsAdminConnection ecsAdminConnection) {
Map
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
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
* @param name
* @param clazz
* @param
* @return
*/
public static
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
public static EcsAdminConnection.EcsAdminConnectionBuilder builder() {
return new EcsAdminConnection.EcsAdminConnectionBuilder();
}
public String getConnectionId() {
return this.connectionId;
}
public Map
return this.dataSource;
}
public void setConnectionId(String connectionId) {
this.connectionId = connectionId;
}
public void setDataSource(Map
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
this.connectionId = connectionId;
this.dataSource = dataSource;
}
public EcsAdminConnection() {
}
public static class EcsAdminConnectionBuilder {
private String connectionId;
private Map
EcsAdminConnectionBuilder() {
}
public EcsAdminConnection.EcsAdminConnectionBuilder connectionId(String connectionId) {
this.connectionId = connectionId;
return this;
}
public EcsAdminConnection.EcsAdminConnectionBuilder dataSource(Map
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();
}
}
```