saas化多租户-动态数据源

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,区分了租户。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神雕大侠mu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值