写点什么

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

作者:weir威尔
  • 2022 年 6 月 23 日
  • 本文字数:4149 字

    阅读完需:约 14 分钟

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

今天要给大家分享的是 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@Unremovablepublic 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("No tenants known at all")); } /** * 当前租户 */ @Override public String resolveTenantId() { return requestTenant.orElseThrow(() -> new RuntimeException("No tenant specified by current request")); }}
复制代码


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@Unremovablepublic class TenantConnections implements TenantConnectionResolver {
private final Map<String, DBConnectionInfo> dbConnectionInfoMap = new HashMap<>();// {// {// put("weir",new DBConnectionInfo("localhost", 3306, "root", "336393", "quarkus-demo"));// put("weir-blog",new DBConnectionInfo("localhost", 3306, "root", "336393", "weirblog"));// }// };
private final Map<String, ConnectionProvider> cache = new HashMap<>();
private static AgroalDataSourceConfiguration createDataSourceConfiguration(DBConnectionInfo dbConnectionInfo) { System.out.println("------------------createDataSourceConfiguration--------------" + 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("jdbc:mysql://" + dbConnectionInfo.getHost() + ":" + dbConnectionInfo.getPort() + "/" + dbConnectionInfo.getDb()) .connectionProviderClassName("com.mysql.cj.jdbc.Driver")// .connectionProviderClassName("org.postgresql.Driver") .principal(new NamePrincipal(dbConnectionInfo.getUser())) .credential(new SimplePassword(dbConnectionInfo.getPassword())))) .get(); }
public Set<String> allTenants() { getTenant(); System.out.println("---------------TenantConnections--------allTenants-------" + 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("select * from SysTenant");
while (resultSet.next()) { dbConnectionInfoMap.put(resultSet.getString("code"), new DBConnectionInfo(resultSet.getString("host"), resultSet.getInt("port"), resultSet.getString("username"), resultSet.getString("password"), resultSet.getString("dbName")));// System.out.println("--------------------------"+resultSet.getString("name")+"--"+resultSet.getString("username")); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
@Override public ConnectionProvider resolve(String tenant) { System.out.println("---------------TenantConnections--------resolve-------" + tenant); if (!dbConnectionInfoMap.containsKey(tenant)) { throw new IllegalStateException("Unknown tenantId: " + 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("Failed to create a new data source based on the tenantId: " + 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@ApplicationScopedpublic class TenantRequestFilter implements ContainerRequestFilter {
@Inject InjectableTenantResolver tenantResolver;
@Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { String tenantId = containerRequestContext.getHeaderString("X-tenant"); if (tenantId != null) { tenantResolver.setRequestTenant(tenantId); } }}
复制代码

quarkus 封装的真是绝了。

看完什么感受,我相信你能看懂,我也相信这个实现要比 spring 体系简单,反正我信。

发布于: 刚刚阅读数: 4
用户头像

weir威尔

关注

www.loveweir.com 2017.12.15 加入

追求真理的勇士

评论

发布
暂无评论
quarkus+saas多租户动态数据源切换实现简单完美_SaaS_weir威尔_InfoQ写作社区