Spring Boot 构建多租户Saas软件架构,实现动态切换数据源

旧版-租户v1.0:实现动态源切换,需手动创建好不同数据源的数据库和表结构;
新版-租户v2.0:实现了动态源切换;且新增租户,动态创建数据库和基础表结构和数据。
 
备注:以上为项目版本,具体可参考 => 源码点击这里

概述

SaaS(Software as a Service), 即多租户(或多承租)软件应用平台。

它只是一种软件架构,并没有多少神秘的东西,也不是什么很难的系统,我个人的感觉,SaaS平台的难度在于商业上的运营,而非技术上的实现。就技术上来说,SaaS是这样一种架构模式:它让多个不同环境的用户使用同一套应用程序,且保证用户之间的数据相互隔离。

应用场景

假设我们需要开发一个应用程序,并且希望将同一个应用程序销售给N家客户使用。在常规情况下,我们需要为此创建N个Web服务器(Tomcat),N个数据库(DB),并为N个客户部署相同的应用程序N次。现在,如果我们的应用程序进行了升级或者做了其他任何的改动,那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来,如果业务开始增长,客户由原来的N个变成了现在的N+M个,我们将面临N个应用程序和M个应用程序版本维护,设备维护以及成本控制的问题。运维几乎要哭死在机房了…

为了解决上述的问题,我们可以开发多租户应用程序,我们可以根据当前用户是谁,从而选择对应的数据库。例如,当请求来自A公司的用户时,应用程序就连接A公司的数据库,当请求来自B公司的用户时,自动将数据库切换到B公司数据库,以此类推。从理论上将没有什么问题,但我们如果考虑将现有的应用程序改造成SaaS模式,我们将遇到第一个问题:如果识别请求来自哪一个租户?如何自动切换数据源?

维护、识别和路由租户数据源

我们可以提供一个独立的库来存放租户信息,如数据库名称、链接地址、用户名、密码等,这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决,下面列举几个常用的方式:

  • 1.可以通过域名的方式来识别租户:我们可以为每一个租户提供一个唯一的二级域名,通过二级域名就可以达到识别租户的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我们识别租户的关键信息。

  • 2.可以将租户信息作为请求参数传递给服务端,为服务端识别租户提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。

  • 3.可以在请求头(Header)中设置租户信息,例如JWT等技术,服务端通过解析Header中相关参数以获得租户信息。

  • 4.在用户成功登录系统后,将租户信息保存在Session中,在需要的时候从Session取出租户信息。

解决了上述问题后,我们再来看看如何获取客户端传入的租户信息,以及在我们的业务代码中如何使用租户信息(最关键的是DataSources的问题)。

我们都知道,在启动Spring Boot应用程序之前,就需要为其提供有关数据源的配置信息(有使用到数据库的情况下),按照一开始的需求,有N个客户需要使用我们的应用程序,我们就需要提前配置好N个数据源(多数据源),如果N<50,我认为我还能忍受,如果更多,这样显然是无法接受的。为了解决这一问题,我们需要借助Hibernate 5提供的动态数据源特性,让我们的应用程序具备动态配置客户端数据源的能力。简单来说,当用户请求系统资源时,我们将用户提供的租户信息(tenantId)存放在ThreadLoacal中,紧接着获取TheadLocal中的租户信息,并根据此信息查询单独的租户库,获取当前租户的数据配置信息,然后借助Hibernate动态配置数据源的能力,为当前请求设置数据源,最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息(租户数据库配置库),其余的数据源动态查询配置。接下来,我们将快速的演示这一功能。

如上话都是参考其他文章的理论,接下来就是实货!!!

项目构建

现在这篇介绍的流程是,我们把各个数据源的配置信息写在一张数据库表里,从数据库表去加载这些数据源信息,根据我们给每个数据源命名的id去切换数据源,操作对应的数据库。

表结构这里不具体讲解,可参考笔者借鉴过的文章。点击这里

如上提到的独立数据库来存放租户信息(master库)。
tenant_info 表结构如下:
笔者表结构
目前 tenant_info 表数据如下:
表数据
还需要两个或多个租户库,用来测试切换不同数据源读取不同的租户库里面的信息。例如创建tenant1 和 tenant2两个库,都创建一个User表,存放对应租户的不同客户信息。

User 表结构如下:在这里插入图片描述

到这里,数据库模拟场景已经准备完毕,接下来下面就是真正的核心环节!

项目环境及pom依赖我不多说,到时候提供项目demo,参考即可!

先将 源码 拉取下来,这篇文章只将参考源码进行讲解,核心代码都在config文件中!

1、核心类AbstractRoutingDataSource的实现类DynamicDataSource

AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法determineCurrentLookupKey() 决定使用哪个数据源

实现逻辑:

  1. 定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
  2. 把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
  3. 调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。

我们自定义的类 DynamicDataSource主要实现了determineCurrentLookupKey()setTargetDataSources() 这两个方法,并且定义了一个 Map<Object, Object> 存放多数据源的信息,然后将Map信息set到setTargetDataSources()中,再调用一遍afterPropertiesSe()。

determineCurrentLookupKey():实现数据源切换要扩展的方法,该方法的返回值就是项目中所要用的DataSource的key值,拿到该key后就可以在resolvedDataSource中取出对应的DataSource,如果key找不到对应的DataSource就使用默认的数据源,前提是指定了默认数据源。
setTargetDataSources():设置多数据源,入参是Map对象。设置完后记得调用一遍afterPropertiesSe()重置AbstractRoutingDataSource中 resolvedDataSources和resolvedDefaultDataSource。

2、动态数据源上下文DynamicDataSourceContextHolder

用 ThreadLocal (线程局部变量,线程安全) 存放当前租户的key,tenant_info 表的 tenant_id字段,改变 ThreadLocal 的值实现切换数据源,然后在 DynamicDataSource 的 determineCurrentLookupKey()方法中获取ThreadLocal 的值。

1、(无事务下)切换数据源必须在调用数据持久层Dao层之前进行。原因:调用Dao层之前,先执行一遍determineCurrentLookupKey()获取当前租户的key,切换一次数据源。
2、(有事务下)切换数据源必须在调用业务层Service层之前进行,也就是开启事务之前。原因:我们一般在service的实现类的方法上@Transactional事务注解,这里就开启了事务。我们可以在控制层Controller层里切换数据源,如果觉得代码臃肿,还可以使用Aop自定义注解的方式实现切换数据源。

3、初始化动态数据源DynamicDataSourceInit

这个类主要是读取数据库的数据源,实现灵活性(所有指定的默认数据源一般都是独立数据库,也就是有存放租户信息的数据库,才可以读取多租户的数据源信息)。用到的数据源信息也就数据库名称、链接地址、用户名、密码。

1、 @PostConstruct:好多人以为是Spring提供的,其实是Java自己的注解,用来修饰一个 非静态的void()方法 。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
2、加载默认数据源除外的其他数据源,put到DynamicDataSource的Map中(可以在put之前验证该数据源是否可以连接成功,连接成功则put进去,否则抛出异常),然后将Map信息set到setTargetDataSources()方法中,设置完后记得调用一遍afterPropertiesSe()重置AbstractRoutingDataSource中 resolvedDataSources和resolvedDefaultDataSource。

4、初始化Bean配置 MybatisConfig

这个类主要初始化了默认数据源的DataSource(加载application.yml配置文件的数据库配置)、初始化Mybatis的SqlSessionFactoryBean、配置事务管理TransactionManager。

总结:
大概执行过程就是,先在MybatisConfig类中从yml文件读取数据库配置来获取默认数据源,再执行AbstractRoutingDataSource类来设置数据源setTargetDataSources()和重置afterPropertiesSet()。

然后再走DynamicDataSourceInit初始化多租户的动态数据源,配置了默认数据源,则可以连接读取多数据源信息(Dao层之前执行一遍determineCurrentLookupKey(),获取当前数据源。由于我在DynamicDataSourceContextHolder类的ThreadLocal指定了默认数据源的key,或者通过AbstractRoutingDataSource类的setDefaultTargetDataSource指定了默认目标数据源,所以取的数据源是独立数据库的key,即“master”),再执行AbstractRoutingDataSource类来设置数据源setTargetDataSources()和重置afterPropertiesSet()。

然后在UserServiceImpl(无事务下)通过DynamicDataSourceContextHolder的setDataSourceKey(“tenant1”)或setDataSourceKey(“tenant2”)改变ThreadLocal的值,来切换数据源,读取对应租户的客户信息。

可别忘了在启动类上配置这个!

//不让Spring自动加载数据源配置,我们将采取手动配置数据源的方式
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

源码点击这里 ,这是小编的个人笔记哦,喜欢的给小编给个赞!!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐