一、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("初始化多租户数据库配置完成...");
}
}