天天看点

多租户架构的动态数据源方案

作者:Esgoon

本文首先介绍了多租户架构(Multi-Tenant)系统数据隔离的几种方案,其后介绍了基于SpringBoot、MySQL的一种动态数据源的实现。

多租户架构数据隔离

多租户架构数据隔离通常有三种实现方案。

1、独立数据库

独立数据库即一个租户(Tenant)一个数据库(DB),这种方案的数据隔离级别最高,安全性最好,可以满足不同租户的独特需求,发生故障时恢复数据比较简单且不会影响到其他租户。但相应的成本也高。

多租户架构的动态数据源方案

注意:这里所讲的数据库与我们日常开发沟通中所说的数据库的概念是有区别的,这里所指独立数据库的意思是一个数据库服务实例,通常对应于一套物理服务器。而我们开发沟通中说的数据库通常是指数据库实例中的一个Schema。

2、共享数据库,独立Schema

这种实现方案是多个租户甚至所有租户共享同一个数据库,但每个租户独立使用Schema。这种方案的优点是一个数据库实例可以支持更多的租户数量。缺点是发生故障时恢复困难,且容易影响到其他租户。

多租户架构的动态数据源方案

3、共享数据库共享Schema

多租户架构的动态数据源方案

这种方案是所有租户共享同一个数据库、同一个Schema,不同租户的数据存放到相同的数据库表中,数据库表通常会增加tenant_id字段用以区分租户的数据。这是共享级别最高,隔离级别最低的方式。

多租户架构的动态数据源方案

以上三种数据隔离方案中,第一种独立数据库我们通常称为物理隔离,其他两种我们称为逻辑隔离。

技术实现方面,独立数据库和独立Schema这两个方式都会涉及到不同的租户对应不同的数据库连接URL、数据库访问账号和密码。也就是说,多租户系统使用过程中,不同租户下的用户使用的是不同的数据源(DataSource),用户的每一次数据库操作,都需要首先根据用户身份(所属租户的tenant_id)动态获取到相应的数据源。

动态数据源

本节以后端SpringBoot、MySQL技术讲解一种动态数据源的实现方案。

在一个多租户系统中,数据存储采用独立数据库或是独立Schema的隔离方式无疑是最灵活的方案,方便数据库层的扩展,且使得各个租户之间互不影响。系统实现方面,也就需要动态数据源支撑。

所谓动态数据源,是需要后端服务在运行过程中,根据操作用户的身份,即所属的租户,动态获取到对应于这个租户的数据库连接,从而达到对数据的操作是发生在正确的目标数据库的目的。

在独立数据库或独立Schema的数据隔离方案中,因为每一个租户都有其自己的数据源信息,如JDBC URL、数据库用户名/密码、甚至是最大连接数、最小连接数等等,首先用一个表将这些信息维护起来。在生产环境中,可以是通过SQL脚本或一个后台管理页面进行维护。

多租户架构的动态数据源方案

存储数据源的表应该位于一个独立的数据库,作为一种公共资源,与用户业务数据库分开。提供一个独立的后端服务(假设命名为tenant服务),对该数据源表进行维护。其他的业务服务通过tenant服务提供的接口获取数据源信息。

数据源的加载

接下来,需要考虑动态数据源的装载与维护逻辑。需要在SpringBoot服务启动时加载数据源。

定义一个数据源加载类,实现FactoryBean与InitializingBean接口。实现afterPropertiesSet()方法,在该方法中,通过feign调用tenant服务,获取到全部租户的数据源信息。

遍历这些数据源信息并创建DataSource对象,将DataSource对象加入ConcurrentHashMap集合,集合元素的键为tenant_id,值为这个tenant_id所对应的DataSource对象。看到这里,或许大家已基本清楚。没错,后面在根据tenant_id获取数据源的时候,就是从这个集合中获取。

数据源与租户ID绑定

因为用户发往后端的每一个HTTP请求,都对应一个用户线程。所以,可以使用ThreadLocal存储用户租户标识tenant_id,以便在需要根据tenant_id获取数据源的时候从ThreadLocal中取出当前操作对应的tenant_id, 即ThreadLocal.get()。

多租户架构的动态数据源方案

在用户登录操作中,登录成功后,将用户的tenant_id信息通过ThreadLocal传递下去:ThreadLocal.set(tenant_id), 在需要使用tenant_id的时候,都可以通过ThreadLocal.get()获取到。

动态获取数据源

重点来了,我们知道,对数据库进行操作,首先要获取数据库连接。动态数据源就是要在运行时动态获取相应的数据源。我们要在实现数据库连接的地方实现自己的逻辑。需要自定义一个javax.sql.DataSource类,实现其中的getConnection()接口方法。主要就是从上面的ConcurrentHashMap取出相应的DataSource。

最后,将自定义的javax.sql.DataSource对象传给DataSourceTransactionManager。动态数据源的主要流程就基本实现完成。概括下实现流程如下。

多租户架构的动态数据源方案

总结

在多租户架构的系统中,如果我们采用独立数据库或是独立Schema的数据隔离方式,动态数据源将是一项重要的基础功能。需要我们熟悉这些知识点,例如FactoryBean、InitializingBean的扩展与加载机制、ThreadLocal特性、DataSourceTransactionManager与javax.sql.DataSource的关系等等,对于理解这个动态数据源实现方案就变得容易了。

继续阅读