这段时候在准备从零开始做一套SaaS系统,之前的经验都是开发单数据库系统并没有接触过SaaS系统,所以接到这个任务的时候也有也些头疼,不过办法部比困难多,难得的机会。

在网上找了很多关于SaaS的资料,看完后使我受益匪浅,写文章之前也一直在关注SaaS系统的开发,通过几天的探索也有一些方向,初步用到了以下技术栈 & 工具:

  • SpringBoot
  • Spring Cloud
  • Spring Security(鉴权)
  • Mybatis Plus(多租户sql增强)
  • 阿里云 Rds(动态创建租户数据库)

多租户系统首先要解决的问题就是如何组织租户的数据问题,通常情况有三种解决方案:

按数据的隔离级别依次为:

  1. 一个租户一个数据库实例(数据库级)
  2. 一个租户一个Schema (Schema)
  3. 每个租户都存储在一个数据库 (行级)

以上三种数据组织方案网上都有一些介绍,就不多啰嗦了。理解三种隔离模式后,起初觉得还是蛮简单的真正开始实施的时候困难不少。

租户标识接口

定义一个TenantInfo来标识租户信息,关于获取当前租户的方式,后面会再提到。

public interface TenantInfo {/*** 获取租户id* @return*/Long getId();/*** 租户数据模式* @return*/Integer getSchema();/*** 租户数据库信息* @return*/TenantDatabase getDatabase();/*** 获取当前租户信息* @return*/static Optional<TenantInfo> current(){return Optional.ofNullable(TenantInfoHolder.get());}
}

DataSource 路由

以前开发的系统基本都是一个DataSource,但是切换为多租户后我暂时分了两种数据源:

  • 租户数据源(TenantDataSource)
  • 系统数据源(SystemDataSource)

起初我的设想是使用Schema级但是由于是使用的Mysql中的SchemaDatabase是差不多的概念,所以后来的实现是基于数据库级的。使用数据库级的因为是系统是基于企业级用户的,数据都比较重要,企业客户很看重数据安全性方面的问题。

下面来一步步的解决动态数据源的问题。

DataSource 枚举


public enum DataSourceType {/*** 系统数据源*/SYSTEM,/*** 多租户数据源*/TENANT,
}

DataSource 注解

定义DataSourceType枚举后,然后定义一个DataSource注解,名称可以随意,一时没想到好名称,大家看的时候不要跟javax.sql.DataSource类混淆了:

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DataSource {/*** 数据源key* @return*/com.csbaic.datasource.core.DataSourceType value() default com.csbaic.datasource.core.DataSourceType.SYSTEM;}

处理 SpringBoot 自动装配的 DataSource

如果你熟悉SpringBoot,应该知道有一个DataSourceAutoConfiguration配置会自动创建一个javax.sql.DataSource,由于在多租户环境下随时都有可能要切换数据源,所以需要将自动装配的javax.sql.DataSource替换掉:

@Slf4j
public class DataSourceBeanPostProcessor implements BeanPostProcessor {@Autowiredprivate  ObjectProvider<RoutingDataSourceProperties> dataSourceProperties;@Autowiredprivate  ObjectProvider<TenantDataSourceFactory> factory;@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if(bean instanceof DataSource){log.debug("process DataSource: {}", bean.getClass().getName());return new RoutingDataSource((DataSource) bean, factory, dataSourceProperties);}return bean;}
}

基于BeanPostProcessor的处理,将自动装配的数据源替换成RoutingDataSource,关于RoutingDataSource后面会再提到。这样可将自动装配的数据源直接作为系统数据源其他需要使用数据源的地方不用特殊处理,也不需要在每个服务中排除DataSourceAutoConfiguration的自动装配。

使用 ThreadLocal 保存数据源类型

数据源的切换是根据前面提到的数据源类型枚举DataSourceType来的,当需要切换不到的数据源时将对应的数据源类型设置进ThreadLocal中:


public class DataSourceHolder {private static final ThreadLocal<Stack<DataSourceType>> datasources = new ThreadLocal<>();/*** 获取当前线程数据源* @return*/public static DataSourceType get(){Stack<DataSourceType> stack = datasources.get();return stack != null ? stack.peek() : null;}/*** 设置当前线程数据源* @param type*/public static void push(DataSourceType type){Stack<DataSourceType> stack = datasources.get();if(stack == null){stack = new Stack<>();datasources.set(stack);}stack.push(type);}/*** 移除数据源配置*/public static void remove(){Stack<DataSourceType> stack = datasources.get();if(stack == null){return;}stack.pop();if(stack.isEmpty()){datasources.remove();}}}

DataSourceHolder.datasources是使用的Stack而不是直接持有DataSource这样会稍微灵活一点,试想一下从方法A中调用方法B,A,B方法中各自要操作不同的数据源,当方法B执行完成后,回到方法A中,如果是在ThreadLocal直接持有DataSource的话,A方法继续操作就会对数据源产生不确定性。

AOP 切换数据源

要是在每个类方法都需要手机切换数据源,那也太不方便了,得益于AOP编程可以在调用需要切换数据源的方法的时候做一些手脚:


@Slf4j
@Aspect
public class DataSourceAspect {@Pointcut(value = "(@within(com.csbaic.datasource.annotation.DataSource) || @annotation(com.csbaic.datasource.annotation.DataSource)) && within(com.csbaic..*)")public void dataPointCut(){}@Before("dataPointCut()")public void before(JoinPoint joinPoint){Class<?> aClass = joinPoint.getTarget().getClass();// 获取类级别注解DataSource classAnnotation = aClass.getAnnotation(DataSource.class);if (classAnnotation != null){com.csbaic.datasource.core.DataSourceType dataSource = classAnnotation.value();log.info("this is datasource: "+ dataSource);DataSourceHolder.push(dataSource);}else {MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method method = methodSignature.getMethod();DataSource methodAnnotation = method.getAnnotation(DataSource.class);if (methodAnnotation != null){com.csbaic.datasource.core.DataSourceType dataSource = methodAnnotation.value();log.info("this is dataSource: "+ dataSource);DataSourceHolder.push(dataSource);}}}@After("dataPointCut()")public void after(JoinPoint joinPoint){log.info("执行完毕!");DataSourceHolder.remove();}
}

DataSourceAspect很简单在有com.csbaic.datasource.annotation.DataSource注解的方法或者类中切换、还原使用DataSourceHolder类切换数据源。

动态获取、构造数据源

前面说了那么多都是在为获取、构建数据源做准备工作,一但数据源切换成功,业务服务获取数据时就会使用javax.sql.DataSource获取数据库连接,这里就要说到RoutingDataSource了:


@Slf4j
public class RoutingDataSource extends AbstractDataSource {/*** 已保存的DataSource*/private final DataSource systemDataSource;/*** 租户数据源工厂*/private final ObjectProvider<TenantDataSourceFactory> factory;/*** 解析数据源* @return*/protected DataSource resolveDataSource(){DataSourceType type =  DataSourceHolder.get();RoutingDataSourceProperties pros = properties.getIfAvailable();TenantDataSourceFactory tenantDataSourceFactory = factory.getIfAvailable();if(tenantDataSourceFactory == null){throw new DataSourceLookupFailureException("租户数据源不正确");}if(pros == null){throw new DataSourceLookupFailureException("数据源属性不正确");}if(type == null){log.warn("没有显示的设置数据源,使用默认数据源:{}", pros.getDefaultType());type = pros.getDefaultType();}log.warn("数据源类型:{}", type);if(type == DataSourceType.SYSTEM){return systemDataSource;}else if(type == DataSourceType.TENANT){return tenantDataSourceFactory.create();}throw new DataSourceLookupFailureException("解析数据源失败");}
}

resolveDataSource方法中,首先获取数据源类型:

 DataSourceType type =  DataSourceHolder.get();

然后根据数据源类型获取数据源:

if(type == DataSourceType.SYSTEM){return systemDataSource;}else if(type == DataSourceType.TENANT){return tenantDataSourceFactory.create();}

系统类型的数据源较简单直接返回,在租户类型的数据时就要作额外的操作,如果是数据库级的隔离模式就需要为每个租户创建数据源,这里封装了一个TenantDataSourceFactory来构建租户数据源:

public interface TenantDataSourceFactory {/*** 构建一个数据源* @return*/DataSource create();/*** 构建一个数据源* @return*/DataSource create(TenantInfo info);
}

实现方面大致就是从系统数据源中获取租户的数据源配置信息,然后构造一个javax.sql.DataSource

注意:租户数据源一定要缓存起来,每次都构建太浪费。。。

小结

经过上面的一系统配置后,相信切换数据已经可以实现了。业务代码不关心使用的数据源,后续切换成隔离模式也比较方便。但是呢,总觉得只支持一种隔离模式又不太好,隔离模式更高的模式也可以作为收费项的麻。。。

使用 Mybatis Plus 实现行级隔离模式

上前提到动态数据源都是基于数据库级的,一个租户一个数据库消耗还是很大的,难达到SaaS的规模效应,一但租户增多数据库管理、运维都是成本。

比如有些试用用户不一定用购买只是想试用,直接开个数据库也麻烦,况且前期开发也麻烦的很,数据备份、还原、字段修改都要花时间和人力的,所以能不能同时支持多种数据隔离模式呢?答案是肯定的,利益于Mybatis Plus可的多租户 SQL 解析器以轻松实现,详细文档可参考:

多租户 SQL 解析器:https://mp.baomidou.com/guide/tenant.html

只需要配置TenantSqlParserTenantHandler就可以实现行级的数据隔离模式:

public class RowTenantHandler implements TenantHandler {@Overridepublic Expression getTenantId(boolean where) {TenantInfo tenantInfo = TenantInfo.current().orElse(null);if(tenantInfo == null){throw new IllegalStateException("No tenant");}return new LongValue(tenantInfo.getId());}@Overridepublic String getTenantIdColumn() {return TenantConts.TENANT_COLUMN_NAME;}@Overridepublic boolean doTableFilter(String tableName) {TenantInfo tenantInfo = TenantInfo.current().orElse(null);//忽略系统表或者没有解析到租户id,直接过滤return tenantInfo == null || tableName.startsWith(SystemInfo.SYS_TABLE_PREFIX);}
}

回想一下上面使用的TenantDataSourceFactory接口,对于行级的隔离模式,构造不同的数据源就可以了。

如何解析当前租户信息?

多租户环境下,对于每一个http请求可能是对系统数据或者租户数据的操作,如何区分租户也是个问题。

以下列举几种解析租户的方式:

  • 系统为每个用户生成一个二级域名如:tenant-{id}.csbaic.com业务系统使用HostOriginX-Forwarded-Host等请求头按指定的模式解析租户
  • 前端携带租户id参数如:http://www.javaobj.com/?tenantId=xxx
  • 根据请求uri路径获取如:http://www.javaobj.com//api/{tenantId}
  • 解析前端传递的token,获取租户信息
  • 租户自定义域名解析,有些功能租户可以绑定自己的域名

解析方式现在大概只知道这些,如果有好的方案欢迎大家补充。为了以为扩展方便定义一个TenantResolver接口:


/*** 解析租户*/
public interface TenantResolver {/*** 从请求中解析租户信息* @param request 当前请求* @return*/Long resolve(HttpServletRequest request);
}

然后可以将所有的解析方式都聚合起来统一处理:

/**** @param domainMapper* @return*/@Beanpublic TenantResolver tenantConsoleTenantResolver(TenantDomainMapper domainMapper, ITokenService tokenService){return new CompositeTenantResolver(new SysDomainTenantResolver(),new RequestHeaderTenantResolver(),new RequestQueryTenantResolver(),new TokenTenantResolver(tokenService),new CustomDomainTenantResolver(domainMapper));}

最后再定义一个Filter来调用解析器,解析租户:

public class UaaTenantServiceFilter implements Filter {private final TenantInfoService tenantInfoService;public UaaTenantServiceFilter(TenantInfoService tenantInfoService) {this.tenantInfoService = tenantInfoService;}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {//从request解析租户信息try{TenantInfo tenantInfo = tenantInfoService.getTenantInfo((HttpServletRequest) request);TenantInfoHolder.set(tenantInfo);chain.doFilter(request,response);}finally {TenantInfoHolder.remove();}}
}

TenantInfoService是获取租户信息的接口,内部还是通过TenantResolver来解析租户Id,然后通过id从系统数据库获取当前租户的信息。

总结

解决完动态数据源、租户信息获取两个问题后,只是一小步,后续还有很多问题需要处理如:系统权限和租户权限、统一登陆和鉴权、数据统计等等。。。,相信这些问题都会解决的,后续再来分享。

推荐阅读

  • 十分钟入门RocketMQ
  • Spring Boot 构建多租户 SaaS 平台核心技术指南
  • Redis 缓存和MySQL数据一致性方案详解
  • Nginx 限流配置
  • 深入探秘 Netty、Kafka中的零拷贝技术!

学习资料分享

12 套 微服务、Spring Boot、Spring Cloud 核心技术资料,这是部分资料目录:

  • Spring Security 认证与授权
  • Spring Boot 项目实战(中小型互联网公司后台服务架构与运维架构)
  • Spring Boot 项目实战(企业权限管理项目))
  • Spring Cloud 微服务架构项目实战(分布式事务解决方案)

公众号后台回复arch028获取资料::

SaaS 系统架构,租户数据隔离模式与租户信息解析方案!相关推荐

  1. SAAS系统架构之数据存储方案

    一. 独立数据库 概述:一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本也高. 这种方案与传统的一个客户.一套数据.一套部署类似,差别只在于软件统一部署在运营商那里.如果面对的是 ...

  2. 实战saas系统多租户数据隔离(三)每个租户使用独立的表空间

    目录 0. 前言 1. 需求分析 2. 系统架构设计 3. 环境准备 4. 编码实现 4.1 添加父项目依赖坐标 4.2 实现eureka注册中心 4.3 实现zuul网关 4.4 实现用户微服务mt ...

  3. 多租户 Saas 系统架构的设计思路

    ToB Saas 系统最近几年都很火.很多创业公司都在尝试创建企业级别的应用 cRM, HR,销售, Desk Saas系统.很多Saas创业公司也拿了大额风投.毕竟Saas相对传统软件的优势非常明显 ...

  4. Saas系统架构的思考,多租户Saas架构设计分析

    ToB Saas系统最近几年都很火.很多创业公司都在尝试创建企业级别的应用 cRM, HR,销售, Desk Saas系统.很多Saas创业公司也拿了大额风投.毕竟Saas相对传统软件的优势非常明显. ...

  5. 租户隔离怎么做MYSQL_一种SaaS软件租户数据隔离的方法与流程

    本发明涉及计算机技术领域,尤其涉及一种SaaS软件租户数据隔离的方法. 背景技术: SaaS是Software-as-a-Service的简称,随着互联网技术的发展和应用软件的成熟, 在21世纪开始兴 ...

  6. 千万级常规saas系统架构精讲(干货)

    什么是saas系统 saas这个概念来源于云计算领域,其本质是软件即服务.要理解这个概念需要从历史说起,对于早期的软件行业,一般是A公司需要一套进销存系统则软件公司就会针对A的需求开发一套进销存系统, ...

  7. 多租户数据隔离的三种方案

    一.多租户在数据存储上存在三种主要的方案,分别是: 1. 独立数据库 这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高. 优点: 为不同的租户提供独立的数据 ...

  8. SAAS系统架构之成熟度模型

    1.概述 对于SAAS应用的架构师而言,尤其是从传统软件转型到SAAS的架构师,遇到的首要挑战就是多租户思维的转变.传统软件的销售模式决定了软件的每一个运行实例都服务于一个客户,因此对于性能.可配置性 ...

  9. sdn框架的计算机网络管理,清华SDN实践--SDN 系统架构与数据中心应用

    清华大学在SDN 的系统架构以及其在数据中心网络中的应用方面展开了深入研究,主要研究成果包括:1. 以数据为中心的软件定义网络架构 SODA(Software Defined Data Centric ...

最新文章

  1. iOS UIImageView 加载含有汉字的url处理方法
  2. 逻辑回归python sigmoid(z)_python实现吴恩达机器学习练习2(逻辑回归)-data1
  3. ORA-07445导致实例崩溃的解决【The solution of instance crush by ORA-07445】
  4. 【控制】《鲁棒控制-线性矩阵不等式处理方法》-俞立老师-第11章-大系统的分散控制
  5. cookie的设置、获取以及删除
  6. 浅谈ASP.NET Forms验证
  7. php flash chart,openflashchart 2.0 简单案例php版
  8. mac 下用 brew 安装mongodb
  9. 使用Nginx反向代理来实现简单的负载均衡
  10. 多线程中的线程安全问题
  11. (十六)java中的String
  12. sun.misc jar包
  13. myeclipse 快捷键(转载)
  14. hdu-3333-Turing Tree(树状数组)
  15. 集成百度做敏感词鉴定
  16. concat函数_《MySQL 入门教程》第15篇MySQL常用函数之字符函数
  17. svg齿轮动画js特效
  18. 不属于php语言的,PHP和Java都不属于脚本语言。()
  19. 转化率中找淘宝店铺推广方法(转)
  20. 图像边缘检测 Canny边缘检测

热门文章

  1. 微服务服务降级与熔断
  2. 基于不可否认技术的珍贵古籍线上交易系统设计与实现(JavaWeb的图书商城系统)
  3. java—使用反射对类进行操作(Class对象获取类名,包名,父类,接口,构造方法,普通方法,属性)
  4. java入门之参考书
  5. 《C#零基础入门之百识百例》(十)其他运算符和优先级 -- 解方程式
  6. python汉字拼音查询_python处理汉字转拼音pypinyin
  7. Mysql高级部分系列(二)
  8. [软件下载]SQL2000示例数据库
  9. 香港公司和大陆公司有什么区别?
  10. java中系统撤销对象顺序实例,撤消/重做功能Java的对象序列化