多租户多数据源开发
背景
项目中遇到一个多租户的需求,集团公司下有多个分公司,每个分公司的业务独立,但是管理上一样,并且集团公司可以动态添加分公司。现有三种方案:
- 在数据库表上添加一个company_id。
- 使用同一个数据库,每个公司用不同的表,使用前缀或后缀区分。
- 每个分公司创建一个数据库。
方案一的优点:只需要一个数据库连接,数据库维护简单,可以动态添加新分公司。缺点:开发过程中需要将所有查询都添加上company_id,非常不方便,并且单表的数据量大,风险大。
方案二的优点:只需要一个数据库连接,各个分公司的数据独立。缺点:表维护麻烦,数据库挂掉,所有分公司数据都挂。
方案三的优点:每个分公司的数据独立,风险小,单库的数据量小。缺点:表维护麻烦。
经讨论后使用了第三种方案,每个分公司创建一个数据库。
方案实现
在方案三的基础上,选择对应的技术框架,在网上找了下,有mycat数据库中间件。但是mycat无法动态添加新的数据库节点,最后选择了自己实现动态添加数据源的方式。
实现思路
springboot启动的时候,创建一个默认的数据库连接(我创建的是集团的),然后从集团公司的数据库中获取分公司的数据库配置动态创建数据源中进行管理,通过当前登陆的用户选择数据源。
实现代码
spring中提供了 AbstractRoutingDataSource 类进行多数据源选择,所以我们在springboot中只需要继承该类,重写选择数据源方法和设置数据源方法,并创建添加数据源方法,实现代码如下:
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.HashMap;
import java.util.Map;
public class MultiRouteDataSource extends AbstractRoutingDataSource {
private static Map<Object, Object> targetDataSources = new HashMap<>();
@Override
protected Object determineCurrentLookupKey() {
// 通过绑定线程的数据源上下文实现多数据源的动态切换
return DataSourceContext.getDataSource();
}
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
MultiRouteDataSource.targetDataSources = targetDataSources;
}
/**
* 动态增加数据源
*
* @param name 数据源名称
* @param dataSource 数据源属性
* @return
*/
public synchronized void addDataSource(String name, HikariDataSource dataSource) {
try {
// 对该数据源进行测试
Map<Object, Object> targetMap = MultiRouteDataSource.targetDataSources;
targetMap.put(name, dataSource);
this.afterPropertiesSet();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}
补充DataSourceContext的内容
/**
* 数据源管理器Bean
*/
public class DataSourceContext {
/**
* 存储的是数据源的key
*/
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
/**
* Get current DataSource
*
* @return data source name
*/
public static String getDataSource() {
return HOLDER.get();
}
/**
* 设置数据源
*
* @param dataSource the dataSource
*/
public static synchronized void setDataSource(String dataSource) {
HOLDER.set(dataSource);
}
/**
* To set DataSource as default
*/
public static void clearDataSource() {
HOLDER.remove();
}
}
创建DataSource配置,该类主要实现的是多数据源的配置。
import com.zaxxer.hikari.HikariDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
/**
* DataSource 自动配置并注册
*
* @return data source
*/
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSource masterDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setPoolName("master");
return dataSource;
}
/**
* 注册动态数据源
*
* @return
*/
@Bean(name = "multiDataSource")
public MultiRouteDataSource multiDataSource() {
MultiRouteDataSource dynamicRoutingDataSource = new MultiRouteDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource());
dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource());// 设置默认数据源
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
return dynamicRoutingDataSource;
}
/**
* session管理器
*
* @return
*/
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(multiDataSource());
return sqlSessionFactoryBean;
}
/**
* 事务管理器
*
* @return the platform transaction manager
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(multiDataSource());
}
}
重点来了,下面代码可以实现在业务类中进行新增数据源和切换数据源。
@Test
public void createDataSource() {
try {
HikariDataSource devDataSource = new HikariDataSource();
devDataSource.setJdbcUrl("");
devDataSource.setDriverClassName(DRIVER_CLASS_NAME);
devDataSource.setUsername("");
devDataSource.setPassword("");
devDataSource.setMinimumIdle(dataSourcePoolProperties.getMinimumIdle());
devDataSource.setMaximumPoolSize(dataSourcePoolProperties.getMaximumPoolSize());
devDataSource.setAutoCommit(dataSourcePoolProperties.isAutoCommit());
devDataSource.setIdleTimeout(dataSourcePoolProperties.getIdleTimeout());
devDataSource.setPoolName(dataSourcePoolProperties.getPoolNamePrefix() + "test");
devDataSource.setMaxLifetime(dataSourcePoolProperties.getMaxLifetime());
devDataSource.setConnectionTimeout(dataSourcePoolProperties.getConnectionTimeout());
devDataSource.setConnectionTestQuery(dataSourcePoolProperties.getConnectionTestQuery());
HikariDataSource testDataSource = new HikariDataSource();
testDataSource.setJdbcUrl("");
testDataSource.setDriverClassName(DRIVER_CLASS_NAME);
testDataSource.setUsername("");
testDataSource.setPassword("");
testDataSource.setMinimumIdle(dataSourcePoolProperties.getMinimumIdle());
testDataSource.setMaximumPoolSize(dataSourcePoolProperties.getMaximumPoolSize());
testDataSource.setAutoCommit(dataSourcePoolProperties.isAutoCommit());
testDataSource.setIdleTimeout(dataSourcePoolProperties.getIdleTimeout());
testDataSource.setPoolName(dataSourcePoolProperties.getPoolNamePrefix() + "test");
testDataSource.setMaxLifetime(dataSourcePoolProperties.getMaxLifetime());
testDataSource.setConnectionTimeout(dataSourcePoolProperties.getConnectionTimeout());
testDataSource.setConnectionTestQuery(dataSourcePoolProperties.getConnectionTestQuery());
// 从容器中获取动态数据源的Bean,这儿也可以通过注解的方式获取
MultiRouteDataSource multiDataSource = (MultiRouteDataSource) SpringContextUtil.getBean("multiDataSource");
// 添加数据源
multiDataSource.addDataSource("dev", devDataSource);
multiDataSource.addDataSource("test", testDataSource);
// 切换数据源
DataSourceContext.setDataSource("test");
// 测试
List<SysUser> sysUsers = sysUserService.getList(null);
log.info(sysUsers.get(0).getName());
} catch (Exception e) {
log.error(e);
}
}
基本代码已经完成,请多指教!