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

原文地址:https://www.cnblogs.com/welisit/p/14043116.html