• 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";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    数据源实体类

    @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    添加拦截器

    @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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    以上就完成了多数据源的代码,测试:
    随便请求一个接口,在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,区分了租户。

  • 相关阅读:
    AI写作工具,智能ai写作工具
    11.应用层数据传输格式/端口号-bite
    【MySQL核心SQL】
    北斗导航 | GNSS数据处理:预处理与参数估计模型
    Spark SQL概述与基本操作
    Netty 学习:通信协议和编解码
    stable diffusion webui中的sampler
    初谈软件工程(一)
    go语言输出带颜色字体
    Python工具箱系列(五)
  • 原文地址:https://blog.csdn.net/m0_37635053/article/details/126735438