当前位置:网站首页>quarkus+saas多租户动态数据源切换实现简单完美

quarkus+saas多租户动态数据源切换实现简单完美

2022-06-23 11:03:00 InfoQ

今天要给大家分享的是quarkus的动态数据源问题,在没有开始之前大家可以想想在spring体系中是怎么实现的都需要写那些代码。
首先大家要明白我们做动态数据源的目的是干嘛的:在一个系统里面来处理不同数据库中的业务需求
有这种业务需求的系统做的是什么项目呢?这个可能就不好总结了,比较普遍的就是多租户大家都知道的独立数据源的模式,还有别的用途这个就比较个性了,只要你们业务需要怎么着都行,比如你们的系统就是要操作不同的数据库等等。
那么动态切换数据源的核心技术原理是什么呢?我个人认为是拦截技术。直白点说就是http一个请求过来肯定是要携带可以用来识别不同数据库连接的信息,那么这个信息就比较灵活比如可以用header信息甚至可以用用户名,你登录系统用户名总是要输入的吧,只要能在数据库检查到用户名就可以发生一系列关于该用户的一切信息,这个也很容易理解。但大多数情况大家还是比较喜欢用header来携带此类信息。
今天说的是quarkus怎么实现动态切换数据源其实无形中也给spring体系做了对比,大家看完就明白了。
视频我做了一期了:头条:
https://www.ixigua.com/i7110837364703887904/
B站:
https://www.bilibili.com/video/BV1nA4y1d7rC?share_source=copy_web
当时基本实现了,今天我觉得是真正的解决了所有的技术难题,可以做到比较完美的效果,当然我并没有做的很完善但是技术难点都解决了,大家可以自己发挥做的更适合自己。
首先说下quarkus官网关于多租户的信息描述地方:
https://quarkus.io/guides/hibernate-orm#multitenancy
编程模式的动态数据源就提到了我们用到的几个接口,当然没有具体实现。这里还要提一下quarkus目前可以实现的模式有两种:一种基于OIDC(OpenID Connect)和我们现在实现的传统模式,他们都是基于header的。
这个图是我实现的动态数据源的逻辑,我想大家都能看懂,我就不一一说了。
我贴出来关键代码,源码大家自己去看(
https://gitee.com/weir_admin/weirblog-quarkus/tree/master/quarkus-tanent2)
package com.weir.quarku.tanent;

import io.quarkus.arc.Unremovable;
import io.quarkus.hibernate.orm.runtime.tenant.TenantResolver;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import java.util.Optional;

/**
 * 识别动态租户信息(一般通过web的request从header传递租户信息)
 * @author weir
 *
 */
@RequestScoped
@Unremovable
public class InjectableTenantResolver implements TenantResolver {

 @Inject
 TenantConnections tenantConnections;

 private Optional<String> requestTenant = Optional.empty();

 public void setRequestTenant(String requestTenant) {
 this.requestTenant = Optional.of(requestTenant);
 }
 /**
 * 默认租户
 */
 @Override
 public String getDefaultTenantId() {
 return tenantConnections.allTenants()
 .stream()
 .findAny()
 .orElseThrow(() -> new RuntimeException(&quot;No tenants known at all&quot;));
 }
 /**
 * 当前租户
 */
 @Override
 public String resolveTenantId() {
 return requestTenant.orElseThrow(() -> new RuntimeException(&quot;No tenant specified by current request&quot;));
 }
}

package com.weir.quarku.tanent;

import io.agroal.api.AgroalDataSource;
import io.agroal.api.configuration.AgroalDataSourceConfiguration;
import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
import io.agroal.api.security.NamePrincipal;
import io.agroal.api.security.SimplePassword;
import io.quarkus.arc.Unremovable;
import io.quarkus.hibernate.orm.runtime.customized.QuarkusConnectionProvider;
import io.quarkus.hibernate.orm.runtime.tenant.TenantConnectionResolver;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;

import static io.agroal.api.configuration.AgroalConnectionPoolConfiguration.ConnectionValidator.defaultValidator;
import static java.time.Duration.ofSeconds;

/**
 * 动态产生并切换数据源连接
 * @author weir
 *
 */
//@PersistenceUnitExtension
@ApplicationScoped
@Unremovable
public class TenantConnections implements TenantConnectionResolver {

private final Map<String, DBConnectionInfo> dbConnectionInfoMap = new HashMap<>();
// {
//  {
//  put(&quot;weir&quot;,new DBConnectionInfo(&quot;localhost&quot;, 3306, &quot;root&quot;, &quot;336393&quot;, &quot;quarkus-demo&quot;));
//  put(&quot;weir-blog&quot;,new DBConnectionInfo(&quot;localhost&quot;, 3306, &quot;root&quot;, &quot;336393&quot;, &quot;weirblog&quot;));
//  }
// };

private final Map<String, ConnectionProvider> cache = new HashMap<>();

private static AgroalDataSourceConfiguration createDataSourceConfiguration(DBConnectionInfo dbConnectionInfo) {
System.out.println(&quot;------------------createDataSourceConfiguration--------------&quot; + dbConnectionInfo);
return new AgroalDataSourceConfigurationSupplier()
.dataSourceImplementation(AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL)
.metricsEnabled(false)
.connectionPoolConfiguration(cp -> cp.minSize(0).maxSize(5).initialSize(0)
.connectionValidator(defaultValidator()).acquisitionTimeout(ofSeconds(5))
.leakTimeout(ofSeconds(5)).validationTimeout(ofSeconds(50)).reapTimeout(ofSeconds(500))
.connectionFactoryConfiguration(cf -> cf
.jdbcUrl(&quot;jdbc:mysql://&quot; + dbConnectionInfo.getHost() + &quot;:&quot; + dbConnectionInfo.getPort()
+ &quot;/&quot; + dbConnectionInfo.getDb())
.connectionProviderClassName(&quot;com.mysql.cj.jdbc.Driver&quot;)
// .connectionProviderClassName(&quot;org.postgresql.Driver&quot;)
.principal(new NamePrincipal(dbConnectionInfo.getUser()))
.credential(new SimplePassword(dbConnectionInfo.getPassword()))))
.get();
}

public Set<String> allTenants() {
getTenant();
System.out.println(&quot;---------------TenantConnections--------allTenants-------&quot; + dbConnectionInfoMap.keySet());
return dbConnectionInfoMap.keySet();
}

@Inject
AgroalDataSource defaultDataSource;

private void getTenant() {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
connection = defaultDataSource.getConnection();
statement = connection.createStatement();
resultSet = statement.executeQuery(&quot;select * from SysTenant&quot;);

while (resultSet.next()) {
dbConnectionInfoMap.put(resultSet.getString(&quot;code&quot;),
new DBConnectionInfo(resultSet.getString(&quot;host&quot;), resultSet.getInt(&quot;port&quot;),
resultSet.getString(&quot;username&quot;), resultSet.getString(&quot;password&quot;),
resultSet.getString(&quot;dbName&quot;)));
// System.out.println(&quot;--------------------------&quot;+resultSet.getString(&quot;name&quot;)+&quot;--&quot;+resultSet.getString(&quot;username&quot;));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

@Override
public ConnectionProvider resolve(String tenant) {
System.out.println(&quot;---------------TenantConnections--------resolve-------&quot; + tenant);
if (!dbConnectionInfoMap.containsKey(tenant)) {
throw new IllegalStateException(&quot;Unknown tenantId: &quot; + tenant);
}

if (!cache.containsKey(tenant)) {
try {
DBConnectionInfo dbConnectionInfo = dbConnectionInfoMap.get(tenant);
AgroalDataSource agroalDataSource = AgroalDataSource
.from(createDataSourceConfiguration(dbConnectionInfo));
QuarkusConnectionProvider quarkusConnectionProvider = new QuarkusConnectionProvider(agroalDataSource);
cache.put(tenant, quarkusConnectionProvider);
return quarkusConnectionProvider;
} catch (SQLException ex) {
throw new IllegalStateException(&quot;Failed to create a new data source based on the tenantId: &quot; + tenant,
ex);
}
}
return cache.get(tenant);
}
}

package com.weir.quarku.tanent;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;

/**
 * 拦截web中header的租户信息并设置给TenantResolver(InjectableTenantResolver)
 * @author weir
 *
 */
@Provider
@ApplicationScoped
public class TenantRequestFilter implements ContainerRequestFilter {

 @Inject
 InjectableTenantResolver tenantResolver;

 @Override
 public void filter(ContainerRequestContext containerRequestContext) throws IOException {
 String tenantId = containerRequestContext.getHeaderString(&quot;X-tenant&quot;);
 if (tenantId != null) {
 tenantResolver.setRequestTenant(tenantId);
 }
 }
}
quarkus封装的真是绝了。
看完什么感受,我相信你能看懂,我也相信这个实现要比spring体系简单,反正我信。
原网站

版权声明
本文为[InfoQ]所创,转载请带上原文链接,感谢
https://xie.infoq.cn/article/d246b203c6bed6dc6fc30781a