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 {

    /**
     * 配置事務
     * @param dynamicDataSource
     * @return
     */
    @Bean
    @Qualifier("transactionManager")
    public PlatformTransactionManager txManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }

    /**
     * 配置文件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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM