Spring Boot下如何实现数据库的多租户

网友投稿 1631 2023-04-23

Spring Boot下如何实现数据库的多租户

Spring Boot下如何实现数据库的多租户

通常情况下,多租户有三种形式:

1、分区(Partitioned)数据:不同租户的数据都在一张表里,通过一个值(tenantId)来区分不同的租户。

2、分结构(Schema):不同的租户数据放置在相同数据库实例的不同结构(Schema)中。

3、分数据库(Database):不同租户的数据放置在不同的数据中。

在Spring Boot中,多租户的能力是由Hibernate提供的,我们在本文中结合Spring Data JPA一起对三种多租户的模式进行演示。本文不需要你具备任何Spring Data JPA和Hibernate基础,也可以通过本文领略下Spring Data JPA。

多租户最最简单也是最常用的多租户方式是“分区数据“”,而这个功能是Hibernate 6.0才具有的功能,而Spring Boot 2.x只支持Hibernate 5.x,所以使用“分区数据”的方式进行多租户需要采用Spring Boot 3.x。幸运的是Spring Boot 3.0将于今年11月份发布,到时候你就可以在生产环境使用本功能了。

“分结构”和“分数据库”的实现方式Spring Boot 2.x是支持的,本文为了演示简单以及远期的前瞻性,将全部以Spring Boot 3.0来实现。

首先,让我们从最简单的例子开始。

一、“分区数据”多租户

新建演示项目:spring-boot-multitenant-partition,依赖为Spring Web、Spring Data JPA、Lombok,Spring Boot版本注意选择3.x,Spring Boot 3.x的最小支持JDK版本为17。

演示实体:

@Entity //1@Datapublic class Person { @Id //2 @GeneratedValue(strategy= GenerationType.IDENTITY) //3 private Long id; @TenantId //4 private String tenantId; private String name; private Integer age;}

1、通过@Entity注解定义一个实体,对应数据库一张表;

2、通过@Id注解表名该属性对应数据库的主键;

3、通过@GeneratedValue(strategy= GenerationType.IDENTITY)配置使用MySQL的主键自增;

4、使用@TenantId注解的属性tenantId作为分区数据多租户的的区分标识。

演示数据访问

import org.springframework.data.jpa.repository.JpaRepository;public interface PersonRepository extends JpaRepository { List findByName(String name);}

这个是Spring Data JPA神奇的地方,通过一个定义一个接口PersonRepository继承框架提供的JpaRepository接口,框架将会给我们自动代理一个实现类,这个实现类除了基本的增删查改以外,还会通过方法名自动推算查询语句。如findByName相当于select * from person where name = ?

演示如何确定TenantId的来源

import org.hibernate.cfg.AvailableSettings;import org.hibernate.context.spi.CurrentTenantIdentifierResolver;import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;@Componentpublic class WiselyTenantIdResolver implements CurrentTenantIdentifierResolver, //1 HibernatePropertiesCustomizer { //2 private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>(); //1.1 public void setCurrentTenant(String currentTenant) { //1.2 CURRENT_TENANT.set(currentTenant); } @Override public String resolveCurrentTenantIdentifier() { //1 return Optional.ofNullable(CURRENT_TENANT.get()).orElse("unknown"); } @Override public boolean validateExistingCurrentSessions() { return false; } @Override public void customize(Map hibernateProperties) { //2 hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this); }}

1.通过实现CurrentTenantIdentifierResolver接口来获取确定TenantId的来源。

1. 1使用线程本地变量CURRENT_TENANT来存储当前的TenantId;

1.2.通过setCurrentTenant方法接受外部设置当前访问者的TenantId,并存储在线程本地变量CURRENT_TENANT中;

1.3.通过重写接口的resolveCurrentTenantIdentifier方法,获得当前的TenantId;

2.通过重写HibernatePropertiesCustomizer接口的customize方法,可以将当前类注册到Hibernate的配置。

通过请求头设置TenantId

@Componentpublic class TenantIdInterceptor implements HandlerInterceptor { private final WiselyTenantIdResolver tenantIdResolver; public TenantIdInterceptor(WiselyTenantIdResolver tenantIdResolver) { this.tenantIdResolver = tenantIdResolver; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { tenantIdResolver.setCurrentTenant(request.getHeader("x-tenant-id")); return HandlerInterceptor.super.preHandle(request, response, handler); }}

注册此***

import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class WebConfig implements WebMvcConfigurer { private final TenantIdInterceptor tenantIdInterceptor; public WebConfig(TenantIdInterceptor tenantIdInterceptor) { this.tenantIdInterceptor = tenantIdInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tenantIdInterceptor); WebMvcConfigurer.super.addInterceptors(registry); }}

演示控制器

@RestController@RequestMapping("/people")public class PersonController { private final PersonRepository personRepository; public PersonController(PersonRepository personRepository) { this.personRepository = personRepository; } @PostMapping public Person save(@RequestBody PersonDto personDto){ //1 return personRepository.save(personDto.createPerson()); } @GetMapping private List all(){ //2 return personRepository.findAll(); }}

1.通过设置在头信息中设置不同的TenantId,数据库中的tenant_id字段将自动存储头中的租户;

2.通过设置在头信息中设置不同的TenantId,只能查询到该租户下的数据。

控制器所需要的DTO

import lombok.Value;@Valuepublic class PersonDto { private String name; private Integer age; public Person createPerson(){ Person person = new Person(); person.setName(this.name); person.setAge(this.age); return person; }}

配置

1.在数据库中创建一个schema叫partitioned;

2.设置为update属性,hibernate会自动因实体类的变化自动创建和更新数据库的表;

启动程序测试需保存的数据

通过postman构造四条数据,分别为:

{"name":"wang","age":22}{"name":"li","age":23}

{"name":"peng","age":24}{"name":"zhang","age":25}

请求需保存的数据

在postman的样子是这样的:

我们一次对上述四条数据进行请求,查看数据库:

我们看见数据都添加了正确的tenant_id。

按租户查询数据

二、“分结构”多租户

演示实体

public class Person { @Id @GeneratedValue(strategy= GenerationType.IDENTITY) private Long id; private String name; private Integer age;}

无须标识租户的tenantId字段,因为租户已经通过schema来隔离开了。

如何获得当前租户id

@Componentpublic class WiselyTenantIdResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer { private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>(); public void setCurrentTenant(String currentTenant) { CURRENT_TENANT.set(currentTenant); } @Override public String resolveCurrentTenantIdentifier() { return Optional.ofNullable(CURRENT_TENANT.get()).orElse("public"); } @Override public boolean validateExistingCurrentSessions() { return false; } @Override public void customize(Map hibernateProperties) { hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this); }}

这里和上例没有什么区别,只是上例设置如果没有获取到TenantId,则将tenant_id字段设置为unknown,本例是将数据库的schema连到public。

通过获得tenantId,连接到对应的schema

import org.hibernate.cfg.AvailableSettings;import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;import javax.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;@Componentpublic class WiselyMultiTenantConnectionProvider implements MultiTenantConnectionProvider, HibernatePropertiesCustomizer { private final DataSource dataSource; public WiselyMultiTenantConnectionProvider(DataSource dataSource){ this.dataSource = dataSource; } @Override public Connection getAnyConnection() throws SQLException { return dataSource.getConnection(); } @Override public void releaseAnyConnection(Connection connection) throws SQLException { connection.close(); } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); connection.createStatement().execute(String.format("use %s;", tenantIdentifier)); return connection; } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { connection.createStatement().execute("use public;"); connection.close(); }//...省略一些非关键方法 @Override public void customize(Map hibernateProperties) { hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this); }}

因为我们是连接单个数据库的不同schema,所以我们只需要在系统中配置一个dataSource,通过这个dataSource我们获得数据库的连接。通过重写MultiTenantConnectionProvider接口的Connection getConnection(String tenantIdentifier)方法,我们根据在上面的WiselyTenantIdResolver获得的tenantId,即当前方法的tenantIdentifier参数来切换dataSource连接到不同的schema.

配置

其余代码和上例保持一致,省略

测试数据和postman和上例保持一致,查看数据库中的数据:

三、“分数据库”多租户

第三个例子是不同租户的数据分别在不同的数据库里,为了演示方便,本例还是用2个schema来模拟两个数据库,区别是相同数据库时我们使用一个dataSource来切换不同的schema;而本例中会有多个dataSource,使用tenantId来切换到不同的dataSource。

spring-jdbc包为我们提供一个类叫做AbstractRoutingDataSource,它可以设置多个数据源,并通过一个key来切换这个数据源,很显然,这个key是我们的tenantId。

动态切换数据源

1、注入WiselyTenantIdResolver的bean获得当前的tenantId;

2、添加默认数据源到动态路由数据源里;

3、定义一个Map,在里面存储不同租户的数据源;

4、通过代码编程的方式构建一个数据源;

5、将这些数据源都添加到动态路由数据源里;

6、通过从WiselyTenantIdResolver中拿到的TenantId,切换到不同的数据源。

在数据源被切换后,我们就可以轻松获得数据库的连接

@Componentpublic class WiselyMultiTenantConnectionProvider implements MultiTenantConnectionProvider, HibernatePropertiesCustomizer { private final DataSource dataSource; public WiselyMultiTenantConnectionProvider(DataSource dataSource){ this.dataSource = dataSource; } @Override public Connection getAnyConnection() throws SQLException { return dataSource.getConnection(); } @Override public void releaseAnyConnection(Connection connection) throws SQLException { connection.close(); } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { return dataSource.getConnection(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { connection.close(); } // 省略不重要的方法 @Override public void customize(Map hibernateProperties) { hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this); }}

这里直接注入前面切换数据源后的得到的dataSource,从dataSource中得到数据库的连接

其余代码与上例保持一致,数据源是编程获得,所以无须在配置中配置。

演示效果和上例一致,再此就不做演示了

四、源码地址

五、参考资料

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:什么是实时流处理?实时流处理技术有哪些?
下一篇:云原生存储:构建高可用、高性能的云原生应用基石
相关文章