• springboot搭建SaaS多租户动态数据源


    一、SAAS是什么

    SaaS是Software-as-a-service(软件即服务)它是一种通过Internet提供软件的模式,厂商将应用软件统一部署在自己的服务器上,客户可以根据自己实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得厂商提供的服务。用户不用再购买软件,而改用向提供商租用基于Web的软件,来管理企业经营活动,且无需对软件进行维护,服务提供商会全权管理和维护软件。

    二、SAAS模式有哪些角色

     ①服务商:服务商主要是管理租户信息,按照不同的平台需求可能还需要统合整个平台的数据,作为大数据的基础。服务商在SAAS模式中是提供服务的厂商。

     ②租户:租户就是购买/租用服务商提供服务的用户,租户购买服务后可以享受相应的产品服务。现在很多SAAS化的产品都会划分

     系统版本,不同的版本开放不同的功能,还有基于功能收费之类的,不同的租户购买不同版本的系统后享受的服务也不一样。

    三、SAAS模式有哪些特点

     ①独立性:每个租户的系统相互独立。

     ②平台性:所有租户归平台统一管理。

     ③隔离性:每个租户的数据相互隔离。

    在以上三个特性里面,SAAS系统中最重要的一个标志就是数据隔离性,租户间的数据完全独立隔离。

    四、数据隔离有哪些方案

    ①独立数据库

    即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。

    优点

    为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求,如果出现故障,恢复数据比较简单。

    缺点

    增多了数据库的安装数量,随之带来维护成本和购置成本的增加。 如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。

    ②共享数据库,隔离数据架构

    即多个或所有租户共享数据库,但是每个租户一个Schema。

    优点

    为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离,每个数据库可支持更多的租户数量。

    缺点

    如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据 如果需要跨租户统计数据,存在一定困难。

    ③共享数据库,共享数据架构

    即租户共享同一个数据库、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。

    优点

    三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。

    缺点

    隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量,数据备份和恢复最困难,需要逐表逐条备份和还原。

    如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。

    五、基于springboot、mybatis-plus实现动态切换数据源

    以下内容是基于上述方案的第一种方案实现的,每个租户都有自己独立的数据库,在一张数据源表中记录所有租户的数据库连接信息

    1. 自定义动态数据源

    要实现动态切换数据源,首先需要替换掉默认mybatis使用的数据源,我们自己定义一个数据源DynamicDataSource

    springboot 提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源。

    package com.example.tenant.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.example.tenant.dto.TenantDatasourceDTO;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    import javax.sql.DataSource;
    import java.util.Map;
    
    /**
     * 自定义一个数据源
     */
    @Slf4j
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        /**
         * 用于保存租户key和数据源的映射关系,目标数据源map的拷贝
         */
        public Map<Object, Object> backupTargetDataSources;
    
        /**
         * 动态数据源构造器
         * @param defaultDataSource 默认数据源
         * @param targetDataSource 目标数据源映射
         */
        public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSource){
            backupTargetDataSources = targetDataSource;
            super.setDefaultTargetDataSource(defaultDataSource);
            // 存放数据源的map
            super.setTargetDataSources(backupTargetDataSources);
            // afterPropertiesSet 的作用很重要,它负责解析成可用的目标数据源
            super.afterPropertiesSet();
        }
    
        /**
         * 必须实现其方法
         * 动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource
         * 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key
         * 通过key在resolvedDataSources这个map中获取对应的数据源,resolvedDataSources的值是由afterPropertiesSet()这个方法从
         * TargetDataSources获取的
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDBType();
        }
    
        /**
         * 添加数据源到目标数据源map中
         * @param datasource
         */
        public void addDataSource(TenantDatasourceDTO datasource) {
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setUrl(datasource.getUrl());
            druidDataSource.setUsername(datasource.getUsername());
            druidDataSource.setPassword(datasource.getPassword());
            // 将传入的数据源对象放入动态数据源类的静态map中,然后再讲静态map重新保存进动态数据源中
            backupTargetDataSources.put(datasource.getTenantKey(), druidDataSource);
            super.setTargetDataSources(backupTargetDataSources);
            super.afterPropertiesSet();
        }
    
    }
    

    2. mybatis数据源配置

    配置mybatis数据源使用自定义的动态数据源

    @Configuration
    @MapperScan({"com.example.tenant.mapper"})
    public class MybatisConfigurer {
    
        /**
         * 配置文件yml中的默认数据源
         * @return
         */
        @Bean(name = "defaultDataSource")
        @ConfigurationProperties(prefix="spring.datasource")
        public DataSource getDefaultDataSource() {
            return DruidDataSourceBuilder.create().build();
        }
    
        /**
         * 将动态数据源对象放入spring中管理
         * @return
         */
        @Bean
        public DynamicDataSource dynamicDataSource() {
    
            Map<Object, Object> targetDataSources = new HashMap<>();
            log.info("将druid数据源放入默认动态数据源对象中");
            targetDataSources.put(GlobalConstant.TENANT_CONFIG_KEY, getDefaultDataSource());
            return new DynamicDataSource(getDefaultDataSource(), targetDataSources);
        }
    
        /**
         * 数据库连接会话工厂
         * @param dynamicDataSource 自定义动态数据源
         * @return
         * @throws Exception
         */
        @Bean
        public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
            MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
            bean.setDataSource(dynamicDataSource);
            bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                    .getResources("classpath*:mapper/**/*.xml"));
            return bean.getObject();
        }
    
        @Bean
        public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory){
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    

    3. 数据源上下文

    创建数据源上下文用于统一每次请求的数据源,通过threadlocal确保在一个线程内使用同一个数据源

    public class DataSourceContextHolder  {
        private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<String>();
    
        /**
         * 保存租户id
         * @param dbType 租户id
         */
        public static void setDBType(String dbType){
            contextHolder.set(dbType);
        }
    
        public static String getDBType(){
            return contextHolder.get();
        }
    
        public static void clearDBType(){
            contextHolder.remove();
        }
    
    }
    

    4. 初始化数据源

    程序启动时从数据库中读取所有租户的数据库连接配置信息,初始化数据源放入动态数据源对象DynamicDataSource的TargetDataSources中

    @Component
    @Order(value = 1)
    @Slf4j
    public class SystemInitRunner implements ApplicationRunner {
    
        @Resource
        private DatasourceMapper tenantDatasourceMapper;
    
        @Autowired
        private DynamicDataSource dynamicDataSource;
    
        @Override
        public void run(ApplicationArguments args) {
            //租户端不进行服务调用
            log.info("==服务启动后,初始化数据源==");
            //切换默认数据源 即tenant库的数据源,用于查询tenant表中的所有tenant数据库配置
            DataSourceContextHolder.setDBType("default");
            //设置所有数据源信息
            log.info("获取当前数据源:" + DataSourceContextHolder.getDBType());
            List<Datasource> tenantInfoList = tenantDatasourceMapper.selectList(null);
            for (Datasource info : tenantInfoList) {
                TenantDatasourceDTO tenantDatasourceDTO = new TenantDatasourceDTO();
                BeanUtils.copyProperties(info, tenantDatasourceDTO);
                dynamicDataSource.addDataSource(tenantDatasourceDTO);
            }
    
            log.info("动态数据源对象中的所有数据源, 已加载数据源个数: {}", dynamicDataSource.backupTargetDataSources.size());
            log.info("初始化多租户数据库配置完成...");
        }
    }
    

    数据库表结构

    image-20200912201054192

    代码地址:https://gitee.com/welitis/blog_code.git

  • 相关阅读:
    快排 [模板]
    翻硬币
    Euphoria与量子波动速读
    高精度例题
    Div3 595 E
    Div 595 C1 C2
    常用 STL 整理
    CF 595 Div3 B2
    【思维】复杂度均摊+并查集——icpc cerc 2019 Saba1000kg
    离散化+圆直线交点+转化——icpc cerc 2019 D
  • 原文地址:https://www.cnblogs.com/welisit/p/14043116.html
Copyright © 2020-2023  润新知