转:如何使⽤SpringBoot和Flyway建⽴不同数据库的多租户应
多租户应⽤能让不同客户端通过同⼀应⽤程序访问不同的隔离的数据库,客户端之间⽆法查看彼此的数据。这意味着我们必须为每个租户建⽴⼀个单独的数据存储。但是如果我们想对数据库进⾏⼀些统⼀的更改,这是针对为每个租户数据库都需要进⾏的统⼀修改。
本⽂展⽰了⼀种⽅法,该⽅法如何使⽤每个租户的数据源来实现Spring Boot应⽤程序,以及如何使⽤Flyway⼀次对所有租户数据库进⾏更新。
本⽂随附GitHub上的⼯作⽰例代码。
通⽤做法
要实现⼀个应⽤程序中的多个租户⼀起使⽤,需要:
如何将传⼊请求绑定到租户,
如何为当前租户提供数据源,以及如何⼀次为所有租户执⾏SQL脚本。
将请求绑定到租户
当许多不同的租户使⽤该应⽤程序时,每个租户都有⾃⼰的数据。这意味着对发送到应⽤程序的每个请求执⾏的业务逻辑必须与发送请求的租户的数据⼀起使⽤。
这就是我们需要将每个请求分配给现有租户的原因。
有多种将传⼊请求绑定到特定租户的⽅法:
发送tenantId带有请求的URI,
tenantId在JWT令牌中添加,
tenantId在HTTP请求的标头中包含⼀个字段,
还有很多…。
为了简单起见,让我们考虑最后⼀个选项。我们将tenantId在HTTP请求的标头中包含⼀个字段。
在Spring Boot中,为了从请求中读取标头,我们实现了WebRequestInterceptor接⼝。该接⼝使我们能够在Web控制器中接收到请求之前对其进⾏拦截:
@Component
public class HeaderTenantInterceptor implements WebRequestInterceptor {
public static final String TENANT_HEADER = "X-tenant";
@Override
public void preHandle(WebRequest request) throws Exception {
ThreadTenantStorage.Header(TENANT_HEADER));
}
// other methods omitted
}
在该⽅法中preHandle(),我们tenantId从标头读取每个请求,并将其转发给ThreadTenantStorage。
ThreadTenantStorage是包含ThreadLocal变量的存储。通过将tenantIdin 存储在中,ThreadLocal我们可以确保每个线程都有该变量的⾃⼰的副本,并且当前线程⽆法访问另⼀个线程tenantId:
public class ThreadTenantStorage {
private static ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
currentTenant.set(tenantId);
}
public static String getTenantId() {
();
}
public static void clear(){
}
}
配置承租⼈绑定的最后⼀步是让Spring知道我们的:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
private final HeaderTenantInterceptor headerTenantInterceptor;
public WebConfiguration(HeaderTenantInterceptor headerTenantInterceptor) {
this.headerTenantInterceptor = headerTenantInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(headerTenantInterceptor);
}
}
不要使⽤顺序号作为租户ID!
顺序数很容易猜到。作为客户端,您要做的就是在⾃⼰的客户端中添加或减去tenantId,修改HTTP标头,然后访问其他租户的数据。
最好使⽤UUID,因为⼏乎⽆法猜测,⽽且⼈们不会将⼀个租户ID与另⼀个租户ID混淆。更好的是,验证每个请求中登录⽤户是否实际上属于指定的租户。
DataSource为每个租户配置
分离不同租户的数据有不同的可能性。我们可以
为每个租户使⽤不同的架构,或者
为每个租户使⽤完全不同的数据库。
从应⽤程序的⾓度来看,模式和数据库由来抽象DataSource,因此,在代码中,我们可以以相同的⽅式
处理这两种⽅法。
在Spring Boot应⽤程序中,我们通常使⽤前缀配置DataSourcein application.yaml使⽤属性spring.datasource。但是我们只能DataSource⽤这些属性定义⼀个。要定义多个,DataSource我们需要在中使⽤⾃定义属性application.yaml:
tenants:
datasources:
vw:
jdbcUrl: jdbc:h2:mem:vw
driverClassName: org.h2.Driver
username: sa
password: password
bmw:
jdbcUrl: jdbc:h2:mem:bmw
driverClassName: org.h2.Driver
username: sa
password: password
`
在这种情况下,我们为两个租户配置了数据源:vw和bmw。
要让DataSource在我们的代码中访问这些,我们可以使⽤以下⽅法将属性绑定到Spring bean @ConfigurationProperties:
```Java
@Component
@ConfigurationProperties(prefix = "tenants")
public class DataSourceProperties {
private Map<Object, Object> datasources = new LinkedHashMap<>();
public Map<Object, Object> getDatasources() {
return datasources;
}
public void setDatasources(Map<String, Map<String, String>> datasources) {
datasources
.forEach((key, value) -> this.datasources.put(key, convert(value)));
}
public DataSource convert(Map<String, String> source) {
ate()
.("jdbcUrl"))
.
("driverClassName"))
.("username"))
.("password"))
.build();
}
}
DataSourceProperties中,使⽤数据源名称作为键和DataSource对象作为值来构建⼀个Map。现在,我们可以向其中添加新的租户,我们可以向其中添加新的租户,application.yaml并且DataSource在应⽤程序启动时将⾃动为该新租户加载。
Spring Boot的默认配置只有⼀个DataSource。但是,在我们的情况下,我们需要⼀种⽅法来根据tenantIdHTTP请求中的来为租户加载正确的数据源。我们可以使⽤AbstractRoutingDataSource来实现。
AbstractRoutingDataSource可以管理多个DataSources和它们之间的路由。我们可以扩展AbstractRoutingDataSource 到租户之间的路线
Datasource:
spring怎么读取yamlpublic class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
TenantId();
}
每当客户端请求连接时,AbstractRoutingDataSource都会调⽤determineCurrentLookupKey()。当前租户可从ThreadTenantStorage获得,⽅法determineCurrentLookupKey() 将返回此当前租户。这样,TenantRoutingDataSource将到DataSource该租户,并⾃动返回到该数据源的连接。
现在,我们必须将Spring Boot的默认值替换DataSource为TenantRoutingDataSource:
@Configuration
public class DataSourceConfiguration {
private final DataSourceProperties dataSourceProperties;
public DataSourceConfiguration(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
@Bean
public DataSource dataSource() {
TenantRoutingDataSource customDataSource = new TenantRoutingDataSource();
customDataSource.setTargetDataSources(
return customDataSource;
}
}
为了让TenantRoutingDataSource知道要使⽤哪个DataSource,我们将DataSourceProperties的DataSource通过setTargetDataSources()传⼊。
现在,每个HTTP请求都有⾃⼰的请求,DataSource具体取决于HTTP标头中的tenantId了。
⼀次迁移多个SQL模式
如果要使⽤Flyway对数据库状态进⾏版本控制并对其进⾏更改(例如添加列,添加表或删除约束),则必须编写SQL脚本。有了Spring Boot的Flyway⽀持,我们只需要部署应⽤程序,新脚本就会⾃动执⾏以将数据库迁移到新状态。
为了为所有租户的数据源启⽤Flyway,⾸先,我们在application.yaml以下位置禁⽤了Flyway⽤于⾃动迁移的预配置属性:
spring:
flyway:
enabled: false
如果我们不这样做,Flyway将在启动应⽤程序时尝试将脚本迁移到当前DataSource脚本。但是在启动过程中,我们还没有当前租户,因此TenantId()将返回null并导致应⽤程序崩溃。
接下来,我们要将Flyway托管的SQL脚本应⽤于在应⽤程序中定义的所有DataSource。我们可以在
@PostConstruct⽅法对DataSource进⾏迭代:
@Configuration
public class DataSourceConfiguration {
private final DataSourceProperties dataSourceProperties;
public DataSourceConfiguration(DataSourceProperties dataSourceProperties) {
this.dataSourceProperties = dataSourceProperties;
}
@PostConstruct
public void migrate() {
for (Object dataSource : dataSourceProperties
.getDatasources()
.values()) {
DataSource source = (DataSource) dataSource;
Flyway flyway = figure().dataSource(source).load();
flyway.migrate();
}
}
}
⽆论何时启动应⽤程序,现在都会为每个租户的DataSource执⾏SQL脚本。
如果要添加新的租户,则只需放⼊新配置到application.yaml,然后重新启动应⽤程序以触发SQL迁移。新租户的数据库将⾃动更新为当前状
态。
如果我们不想重新编译⽤于添加或删除租户的应⽤程序,则可以将租户的配置外部化(即不要烘焙application.yaml到JAR或WAR⽂件中)。然后,触发Flyway迁移所需的⼀切只是重新启动。
结论
Spring Boot提供了实现多租户应⽤程序的好⽅法。使⽤,可以将请求绑定到租户。Spring Boot⽀持使⽤许多数据源,⽽使⽤Flyway,我们可以跨所有这些数据源执⾏SQL脚本。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。