这段时候在准备从零开始做一套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. SQL查询不重复数据
  2. Https 客户端与服务器交互过程梳理(转)
  3. bat代码小游戏_程序员入职被27岁领导告诫:我被BAT录用过,是算法方面泰斗大哥...
  4. day 96 关于分页的使用
  5. ajax中迭代是什么意思,Ajax 局部刷新迭代器的内容
  6. 数据库 流量切分_私域流量之社群运营技巧,社群运营技巧解析
  7. 文件读写: 二进制方式和文本方式的区别
  8. 最经济方案 谈P2P电影服务器
  9. 微信小程序实现OCR扫描识别
  10. mod mpm event php7.1,CentOS 7 安裝 PHP-FPM 及使用 mod_mpm_event
  11. Gitee-基于Git的代码托管和研发协作平台,JNPF快速开发框架源码目录截图
  12. SQL——汇总分组排序
  13. 百度地图缩放级别与比例尺的关系
  14. 华硕天选2键盘背光灯切换颜色
  15. 服务器测评文档,十年磨一剑,腾讯自研TBase数据库有奖测评
  16. 消除WSL中ls Windows文件夹时背光配色的方法
  17. 牛客网 G-送分了 QAQ 数位 dp入门
  18. mac上将视频变小_如何在Linux上将iPhone的.mov视频旋转90度?
  19. Qt on Android 核心编程
  20. 剑灵灵动区服务器位置,剑灵第四次合服或将来临,终于合大区

热门文章

  1. hon linux lol中文语音包,致lol转来的童鞋们:HON和英雄联盟的不同点
  2. Patran学习问题笔记
  3. Android黑屏死机--充电运行土豆视频【.4.4】》播放视频中黑屏死机》手动按电源键开机显示电量为6%
  4. 编程队伍队名_献礼集团25周年 | 走进编程大赛里的“程序媛”
  5. 你或许理解错了Android系统权限管理的这两个概念
  6. css3设置动画不循环播放,不一样的css3之Animation
  7. 20201025firewall-config以及ssh服务管理远程主机以及双网卡实现网络均衡负载
  8. Nacos 在 Apache APISIX API 网关中的服务发现实践
  9. CentOS 7下简单搭建个人博客——wordpress
  10. pe中怎么卸载服务器系统更新,如何删除驱动,pe下删除系统驱动工具