saas化多租户-动态数据源
摘要:本文主要讲述多数据源配置(即不同租户进来使用对应的数据源),动态添加或删除数据源,本文使用拦截器实现。
1. 基础实体类和常量类
我做实验要用到的常量
public class DbConstants {
public static final String DEFAULT_DB1="1";
public static final String DEFAULT_DB2="2";
public static final String DEFAULT_DB3="3";
public static final String DEFAULT_DB4="4";
}
数据源实体类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DataSourceItem {
private String key;
private String url;
private String username;
private String password;
private String driverClassName;
private String poolName;
}
2.扩展 Spring 的 AbstractRoutingDataSource 抽象类继承类
public class DynamicDataSourceContext extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 数据源存放本地map
*/
public static Map<Object,Object> dataSourceMap = new ConcurrentHashMap<>();
/**
* 设置默认数据源、全部数据源,及刷新
*/
public void freshDataSource(Map<Object, Object> targetDataSources) {
//默认数据源
super.setDefaultTargetDataSource(targetDataSources.get(DbConstants.DEFAULT_DB1));
//设置全部数据源
super.setTargetDataSources(targetDataSources);
//刷新(即把targetDataSources刷到resolvedDataSources中去,resolvedDataSources才是我们真正存放数据源的map)
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
//获取当前指定的数据源
return getDataSource();
}
@Override
public void afterPropertiesSet() {
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
3.配置及初始化多数据源类
@Configuration
@Component
public class DynamicDataSourceConfig {
/**
* 把DynamicDataSourceContext 纳入容器管理,其他地方使用DynamicDataSourceConfig 类可以直接从容器取对象,并调用freshDataSource方法
*/
@Bean
@Primary
public static DynamicDataSourceContext dataSource(){
Map<Object,Object> targetDataSource = getDataSource();
//把DynamicDataSourceContext纳入容器管理
DynamicDataSourceContext dynamicDataSourceContext = new DynamicDataSourceContext();
dynamicDataSourceContext.freshDataSource(targetDataSource);
return dynamicDataSourceContext;
}
/**
* 构建初始化数据源 TODO 生成中去其他地方获取初始化数据源(例如:表里面获取)
* @return
*/
public static Map<Object, Object> getDataSource() {
DataSourceItem ds1 = DataSourceItem
.builder()
.key(DbConstants.DEFAULT_DB1)
.poolName(DbConstants.DEFAULT_DB1)
.url("jdbc:mysql://192.168.183.129:3306/saas-union1?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai")
.username("root")
.password("123456")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
DataSourceItem ds2 = DataSourceItem
.builder()
.key(DbConstants.DEFAULT_DB2)
.poolName(DbConstants.DEFAULT_DB2)
.url("jdbc:mysql://192.168.183.129:3306/saas-union2?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai")
.username("root")
.password("123456")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
DataSourceItem ds3 = DataSourceItem
.builder()
.key(DbConstants.DEFAULT_DB3)
.poolName(DbConstants.DEFAULT_DB3)
.url("jdbc:mysql://192.168.183.129:3306/saas-union3?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai")
.username("root")
.password("123456")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
Map<Object, Object> map = new HashMap<>();
map.put(ds1.getKey(),buildDataSource(ds1));
map.put(ds2.getKey(),buildDataSource(ds2));
map.put(ds3.getKey(),buildDataSource(ds3));
//初始化数据放进本地数据源map
DynamicDataSourceContext.dataSourceMap.put(ds1.getKey(),buildDataSource(ds1));
DynamicDataSourceContext.dataSourceMap.put(ds2.getKey(),buildDataSource(ds2));
DynamicDataSourceContext.dataSourceMap.put(ds3.getKey(),buildDataSource(ds3));
return map;
}
/**
* 把数据源对象组装成HikariDataSource
* @param dataSourceItem
* @return
*/
private static Object buildDataSource(DataSourceItem dataSourceItem) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dataSourceItem.getUrl());
dataSource.setUsername(dataSourceItem.getUsername());
dataSource.setPassword(dataSourceItem.getPassword());
dataSource.setDriverClassName(dataSourceItem.getDriverClassName());
return dataSource;
}
}
4.拦截器配置
@Component
public class DataSourceInterceptor implements HandlerInterceptor {
//从容器获取对象,切忌这里不要自己去new,那样的话会导致对象不一致从而引起数据不一致
@Resource
private DynamicDataSourceContext dynamicDataSourceContext;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//todo 获取instanceId 可以由网关带来
String instanceId = request.getHeader("instanceId");
//注意这里要用容器中那个,与dataSource()中创建的是同一个(之前自己new一个,就一直找不到错误原因)
Map<Object, DataSource> resolvedDataSourcesMap = dynamicDataSourceContext.getResolvedDataSources();
Set<Object> dynamicDataSourceSet = resolvedDataSourcesMap.keySet();
//这里的业务是判断现在租户的instanceId是否已纳入数据源,如果没有就去添加该租户的数据源 TODO 生产中可能会根据instanceId去查表,获取该租户的配置数据源信息(这里演示就自己写个数据源来添加)
if (!dynamicDataSourceSet.contains(instanceId)) {
//获取现在已有的数据源 TODO 根据业务添加或删除数据源
Map<Object, Object> map = new ConcurrentHashMap<>();
for (Object key : resolvedDataSourcesMap.keySet()) {
map.put(key, resolvedDataSourcesMap.get(key));
}
//组装新的数据源(添加或不添加)--
buildDataSources(map, instanceId);
//刷新数据源
dynamicDataSourceContext.freshDataSource(map);
}
//设置当前租户对应的数据库
dynamicDataSourceContext.setDataSource(instanceId);
System.out.println("当前数据源是:" + DynamicDataSourceContext.getDataSource());
return true;
}
private void buildDataSources(Map<Object, Object> map, String instanceId) {
//TODO 远程获取数据源状态,如果是 启用状态则添加 否则不添加 如果已停用就删除
int flag = 1;
if (instanceId.equals("4") && flag == 1) {
DataSourceItem ds = DataSourceItem
.builder()
.key(DbConstants.DEFAULT_DB4)
.poolName(DbConstants.DEFAULT_DB4)
.url("jdbc:mysql://192.168.183.129:3306/saas-union4?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai")
.username("root")
.password("123456")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
map.put(ds.getKey(), buildDataSource(ds));
} else {
//其他状态则认为数据源不可用-不添加
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
DynamicDataSourceContext.clearDataSource();
}
private static Object buildDataSource(DataSourceItem dataSourceItem) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dataSourceItem.getUrl());
dataSource.setUsername(dataSourceItem.getUsername());
dataSource.setPassword(dataSourceItem.getPassword());
dataSource.setDriverClassName(dataSourceItem.getDriverClassName());
return dataSource;
}
}
添加拦截器
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 这里注意不要用new的DataSourceInterceptor ,因为那样不会添加容器中的那个拦截器,而是添加的new的拦截器
* 所以 从容器中获取 全局唯一
*/
@Resource
private DataSourceInterceptor dataSourceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(dataSourceInterceptor).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
以上就完成了多数据源的代码,测试:
随便请求一个接口,在Header带上instanceId,即可获取对应的数据源。如上例子,我初始化添加了key分别为1,2,3的map,
如果instanceId为1则会使用saas-union1,
instanceId为2则会使用saas-union2,
instanceId为3则会使用saas-union3,
instanceId为4,则会把saas-union4添加进数据源并使用它。以上实验都是写死的数据,生产中肯定都是其他地方获取,比如表里面,key我用的1234,生产可以使用instanceId,区分了租户。