环境:SpringBoot3.3.0
多租户表示应用程序的单个运行实例同时为多个客户机(租户)服务的体系结构。这在SaaS解决方案中非常常见。在这些系统中,隔离与各种租户相关的信息(数据、定制等)是一个特殊的挑战。这包括存储在数据库中的每个租户拥有的数据。以下是三种常用的多租户架构实现方案:
图片
每个租户的数据都保存在一个物理上独立的数据库实例中。JDBC连接将专门指向每个数据库,因此任何池都将按租户进行。这里,一种通用的应用程序方法是为每个租户定义JDBC连接池,并根据与当前登录用户相关联的租户标识符来选择要使用的池。
优点:
缺点:
每个租户的数据都保存在单个数据库实例上的不同数据库Schema中。这里有两种不同的定义JDBC连接的方法:
优点:
缺点:
所有数据都保存在一个数据库Schema中。通过使用分区列对每个租户的数据进行分区。这种方法将使用单个连接池为所有租户提供服务。但是,在这种方法中,应用程序需要对每个SQL语句添加分区列(查询时where条件加入分区列作为查询条件)。
优点:
缺点:
接下来我会对分区数据和独立数据库2种架构进行详细的介绍。独立Schema方案其实与独立数据库模式挺像的,如果基于MySQL其实对应的就是不同数据库(可以是同一个MySQL实例,通过use xxx切换数据库),基于Oracle就是对应不同的用户上(并非schema与用户等同)。
注:请先确保你当前使用的SpringBoot版本(Spring Data JPA)整合的Hibernate版本至少是6.0版本以上。
@Entity@Table(name = "t_person")public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id ; private String name ; private Integer age ; @TenantId private String tenantId ;}
这里通过@TenantId注解标注,该字段专门用来分区租户的,Hibernate在查询数据时会自动添加该查询条件,如果你使用的本地SQL(自己编写SQL),那么需要你自行添加该条件(租户ID条件)。
// DAOpublic interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {}// Service@Servicepublic class PersonService { private final PersonRepository personRepository ; public PersonService(PersonRepository personRepository) { this.personRepository = personRepository ; } // 查询所有Person数据 public List<Person> persons() { return this.personRepository.findAll() ; }}
@GetMapping("")public List<Person> persons() { return this.personService.persons() ;}
以上是开发一个业务功能的基本操作,接下来才是重点
该的作用获取当前租户ID,这里基于ThreadLocal实现
public class TenantIdResolver implements CurrentTenantIdentifierResolver<String> { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public void setCurrentTenant(String currentTenant) { CURRENT_TENANT.set(currentTenant); } @Override public String resolveCurrentTenantIdentifier() { // 注意这里不能返回null return Optional.ofNullable(CURRENT_TENANT.get()).orElse("default") ; } @Override public boolean validateExistingCurrentSessions() { return true; }}
上面的组件用来从当前的ThreadLocal中获取租户ID,接下来就是像ThreadLocal存入租户ID。
该拦截器的作用用来从请求Header中获取租户ID,存入ThreadLocal中。
@Componentpublic class TenantIdInterceptor implements HandlerInterceptor { private final TenantIdResolver tenantIdResolver; public TenantIdInterceptor(TenantIdResolver tenantIdResolver) { this.tenantIdResolver = tenantIdResolver; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tenantId = request.getHeader("x-tenant-id"); tenantIdResolver.setCurrentTenant(tenantId); return true ; }}
最后一步就是配置hibernate,设置租户ID的解析器。
spring: jpa: properties: hibernate: '[tenant_identifier_resolver]': 'com.pack.tenant.config.TenantIdResolver'
完成以上类及配置的编写后就实现了基于列区分(分区)的多租户架构方案。
准备数据:
图片
图片
图片
SQL执行情况:
图片
自动添加了tenant_id查询条件。
每租户对应一个数据库,这需要在项目中配置多个数据源,同时提供一个数据源路由的核心类。
你也可以将数据源的信息专门存放在数据表中。
pack: datasource: defaultDs: ds1 config: ds1: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tenant-01 username: tenant01 password: xxxooo type: com.zaxxer.hikari.HikariDataSource ds2: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tenant-02 username: tenant02 password: oooxxx type: com.zaxxer.hikari.HikariDataSource
在Spring实现多数据源切换,可以通过继承AbstractRoutingDataSource。
public class PackRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get() ; }}public class DataSourceContextHolder { private static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>() ; public static void set(String key) { HOLDER.set(key) ; } public static String get() { return HOLDER.get() ; } public static void clear() { HOLDER.remove() ; }}
@Configurationpublic class DataSourceConfig { @Bean public DataSource dataSource(MultiDataSourceProperties properties) { PackRoutingDataSource dataSource = new PackRoutingDataSource(properties.getDefaultDs()) ; Map<Object, Object> targetDataSources = new HashMap<>() ; // PackDataSourceProperties类仅仅就是继承DataSourceProperties Map<String, PackDataSourceProperties> configs = properties.getConfig() ; configs.forEach((key, props) -> { targetDataSources.put(key, createDataSource(props, HikariDataSource.class)) ; }); dataSource.setTargetDataSources(targetDataSources) ; return dataSource ; } private static <T> T createDataSource(PackDataSourceProperties properties, Class<? extends DataSource> type) { // 这里没有考虑池的配置 return (T) properties.initializeDataSourceBuilder().type(type).build(); }}
接下来定义拦截器,设置当前要操作的数据源。
@Componentpublic class TenantIdInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tenantId = request.getHeader("x-tenant-id"); DataSourceContextHolder.set(tenantId) ; return true ; }}
以上就完成了多数据源的所有类及配置的编写。
本文链接:http://www.28at.com/showinfo-26-91376-0.htmlSpringBoot多租户三种架构实现方案详解
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 聊聊主流消息队列的认证和鉴权!