一、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("初始化多租戶數據庫配置完成...");
}
}