前言
saas 软件即服务 现在的软件服务提供商提供一套页面给各个租户,通过一个申请页面
填写租户的租户信息,点击生成,租户就可以有一套自己的系统,可以自己去新建用户,角色,授权等操作。
其实这就是所谓的多租户技术。
多租户,通俗点说,多个租户共用同一套服务提供商提供系统资源,即跟现在流行的共享单车,充电宝差不多。
多租户更多跟云计算在一起,因为你有的客户需求大,付费多,那它分配的计算资源和功能更多,比如有自己独立的应用实例,数据库,硬盘空间等。
这个跟云计算的概念就差不多,云计算就是计算资源,理论上云上的资源的无限大的。
多租户隔离级别
多租户主要就是数据隔离,具体来说有三种:
1. 独立数据库
2. 共享数据库,独立 Schema
3. 共享数据库,共享 Schema,共享数据表
第一种消耗资源最多,就是租户有独立的数据库实例。
第二种在一个数据库实例中每一个租户建立一个Schema数据库,这个也有个问题,当你租户很多的话,对应的表也更多,堆数据库性能也有影响
第三种实例,数据库,表都共享,基于一个租户字段进行隔离,这种成本最低,隔离性也最差。
架构图
第三种网上有很多demo,大部分都是通过mybatis plus 在数据库操作时增加一个租户字段。
第一和第二种差不多,都可以通过动态切换数据源的方法来达到。下面我就只要来讲第二种
下面是一个架构图
如上图所示:租户可以通过多租户系统申请应用和资源,审核通过后,租户信息同步到redis当中,同时根据的租户类型在对应的数据库实例中初始化库和表,
你也可以自己手动在mysql库中自己增加几个一样的数据库,数据初始脚本在项目里。
ps 我的应用系统项目来自 https://github.com/Heeexy/SpringBoot-Shiro-Vue 这是一个简单的springboot +vue的项目,为了增加动态数据源,我讲spring-boot-starter-parent版本升级为2.3.4.RELAERS版本。
可以导入我修改后的代码,下载地址
1,增加了 shrek-tanent模块,改模块是spring-boot-starter模块,通过启动类加入注解@EnableTenant启动租户模式,
主要代码shrek-tanent代码如下:
DynamicRoutingDataSourceHolder.java 动态数据源持有类
public class DynamicRoutingDataSourceHolder {
public static final String PRIMARY_DATASOURCE = "primaryDatasource";
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void putKey(String name) {
THREAD_LOCAL.set(name);
}
public static String getKey() {
String key = THREAD_LOCAL.get();
if (key == null) {
key = PRIMARY_DATASOURCE;
putKey(key);
}
return key;
}
public static void removeKey() {
THREAD_LOCAL.remove();
}
}
DynamicRoutingDataSource.java 动态数据源类
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
//获取当前请求线程的数据源持有
@Override
protected Object determineCurrentLookupKey() {
return DynamicRoutingDataSourceHolder.getKey();
}
//设置多数据源map
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
@Configuration
public class DatasourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
/**
* 设置默认数据源
* @return
*/
@Primary
@Bean(name = "datasource")
public DynamicRoutingDataSource dynamicRoutingDataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(16);
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl(url);
hikariDataSource.setUsername(username);
hikariDataSource.setPassword(password);
hikariDataSource.setDriverClassName(driverClassName);
targetDataSources.put(DynamicRoutingDataSourceHolder.PRIMARY_DATASOURCE, hikariDataSource);
dataSource.setTargetDataSources(targetDataSources);
//设置动态数据源的默认数据源,也就是配置文件里的数据源
dataSource.setDefaultTargetDataSource(hikariDataSource);
return dataSource;
}
/**
* 加入事务管理? 没试过
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicRoutingDataSource());
}
}
@Configuration
@Slf4j
public class DynamicSourceConfig implements ApplicationContextAware {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
public static ApplicationContext applicationContext;
public static List<TenantUser> tenantUsers = new ArrayList();
//演示用 静态初始化, 可以考虑一个定时任务从redis通过,跟我上面的图形一样
static {
tenantUsers.add(new TenantUser(1,"aaa","jdbc:mysql://127.0.0.1:3306/shrek_example_1348915432900841474?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai","root","root"));
tenantUsers.add(new TenantUser(2,"bbb","jdbc:mysql://127.0.0.1:3306/shrek_example_1349659685910249474?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai","root","root"));
}
@PostConstruct
public void init() {
List<TenantUser> list = tenantUsers;
ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) DynamicSourceConfig.applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
Map<Object, DataSource> dataSourceMap = dynamicRoutingDataSource.getResolvedDataSources();
Map<Object, Object> map = new HashMap<>(dataSourceMap);
for (TenantUser user : list) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(HikariDataSource.class);
beanDefinitionBuilder.addPropertyValue("username", user.getDatasourceUsername());
beanDefinitionBuilder.addPropertyValue("password", user.getDatasourcePassword());
beanDefinitionBuilder.addPropertyValue("jdbcUrl", user.getDatasourceUrl());
beanDefinitionBuilder.addPropertyValue("driverClassName", "com.mysql.cj.jdbc.Driver");
String beanName = user.getPrefix();
beanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
DataSource dataSource = DynamicSourceConfig.applicationContext.getBean(beanName, DataSource.class);
map.put(beanName, dataSource);
}
dynamicRoutingDataSource.setTargetDataSources(Collections.unmodifiableMap(map));
log.info("dynamic datasource init success");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
DynamicSourceConfig.applicationContext = applicationContext;
}
}
还有一个拦截类,就是拦截前端的请求,获取分域名,存入的ThredLocal中,这里可以做个判断,可以判断为空,直接返回租户未注册,再根据租户类型是基于字段还是库的,这里我没判断
@Component
public class RequestHander implements HandlerInterceptor {
//请求前
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//获取域名,从本地缓存对象(从redis同步过来)判断是否过期,租户ID和多租户类型,存入ThreadLocal
URL url = new URL(httpServletRequest.getRequestURL().toString());
DynamicRoutingDataSourceHolder.putKey(url.getHost().split("\\.")[0]);
return true;
}
}
修改你的host文件。
127.0.0.1 aaa.shrek.com
127.0.0.1 bbb.shrek.com
127.0.0.1 ccc.shrek.com
启动项目后,不同的域名就会操作不同的数据库了,这样就差不多是可以实现多租户了,一套应用,多个租户共用,可以基于多种数据隔离模式。