当前位置: 首页 > 知识库问答 >
问题:

使用r2dbc的基于多租户模式的应用程序

牟星火
2023-03-14

我正在开发一个多租户反应式应用程序,使用带有r2dbc驱动程序的Spring-Webflow Spring-data-r2dbc连接到Postgresql数据库。多租户部分是基于模式的:每个租户一个模式。因此,根据上下文(例如登录的用户),请求将访问数据库的特定模式。

我正在努力研究如何在r2dbc中实现这一点。理想情况下,这将是Hibernate处理MultiTenantConnectionProvider的方式(参见示例16.3)。

到目前为止,我发现了什么,我做了什么:

>

  • 可以使用这里提到的AbstractRoutingConnectionFactory。但我被迫按租户/模式创建ConnectionFactory。在我看来,这远非高效/可扩展,我宁愿使用像r2dbc-pool这样的连接池
  • 我查看了PostgresqlConnectionFactory。这里有趣的是,在准备连接上有一个对setSchema(连接)的调用:

    private Mono<Void> setSchema(PostgresqlConnection connection) {
        if (this.configuration.getSchema() == null) {
            return Mono.empty();
        }
    
        return connection.createStatement(String.format("SET SCHEMA '%s'", this.configuration.getSchema()))
            .execute()
            .then();
    }
    

    我可能需要找到一种方法来覆盖它,以便从上下文而不是从配置中动态获取模式?

    >

  • 否则,我可以尝试将请求中的架构指定为表前缀:

        String s = "tenant-1";
        databaseClient.execute("SELECT * FROM \"" + s + "\".\"city\"")
                .as(City.class)
                .fetch()
                .all()
    

    但是我不能再使用SpringData了,或者我需要重写每个请求,以将租户作为参数传递。

    任何提示/帮助赞赏:)

  • 共有3个答案

    杨轶
    2023-03-14

    我为r2dbc创建了一个多租户示例,但使用了每个数据库策略。

    在此处检查完整的示例代码。

    在某些数据库中,模式和数据库概念是等效的。如果您坚持使用每模式策略,请在获取连接时添加SQL以选择模式(请研究您正在使用的数据库,并确定设置模式的正确子句)。

    司徒翼
    2023-03-14

    谢谢你的回答。我最终得到了这个解决方案:

    按租户/架构构建ConnectionFactory:

    public class CloudSpringUtilsConnectionFactoryBuilder implements ConnectionFactoryBuilder {
    
    @Override
    public ConnectionFactory buildConnectionFactory(String schema) {
        PostgresqlConnectionConfiguration configuration = getPostgresqlConnectionConfigurationBuilder(schema)
                .build();
        return new PostgresqlConnectionFactory(configuration);
    }
    
    @Override
    public ConnectionFactory buildSimpleConnectionFactory() {
        PostgresqlConnectionConfiguration configuration = getPostgresqlConnectionConfigurationBuilder(null)
                .build();
        return new PostgresqlConnectionFactory(configuration);
    }
    
    protected PostgresqlConnectionConfiguration.Builder getPostgresqlConnectionConfigurationBuilder(String schema) {
        return PostgresqlConnectionConfiguration
                .builder()
                .username(dbUser)
                .password(dbPassword)
                .host(dbHost)
                .port(dbPort)
                .database(dbName)
                .schema(schema);
    }
    

    创建一个TenantTroutingConnectionFactory,以根据租户获得正确的ConnectionFactory。在我们的示例中,租户是从身份验证主体(将令牌转换为用户配置文件)提取的:

    public class TenantRoutingConnectionFactory extends AbstractRoutingConnectionFactory {
    
    private final DatabaseMigrationService databaseMigrationService;
    private final ConnectionFactoryBuilder connectionFactoryBuilder;
    
    private final Map<String, ConnectionFactory> targetConnectionFactories = new ConcurrentHashMap<>();
    
    @PostConstruct
    private void init() {
        setLenientFallback(false);
        setTargetConnectionFactories(new HashMap<>());
        setDefaultTargetConnectionFactory(connectionFactoryBuilder.buildConnectionFactory());
    }
    
    @Override
    protected Mono<Object> determineCurrentLookupKey() {
        return ReactiveSecurityContextHolder.getContext()
                .map(this::getTenantFromContext)
                .flatMap(tenant -> databaseMigrationService.migrateTenantIfNeeded(tenant)
                        .thenReturn(tenant));
    }
    
    private String getTenantFromContext(SecurityContext securityContext) {
        String tenant = null;
        Object principal = securityContext.getAuthentication().getPrincipal();
        if (principal instanceof UserProfile) {
            UserProfile userProfile = (UserProfile) principal;
            tenant = userProfile.getTenant();
        }
        ...
        log.debug("Tenant resolved: " + tenant);
        return tenant;
    }
    
    @Override
    protected Mono<ConnectionFactory> determineTargetConnectionFactory() {
        return determineCurrentLookupKey().map(k -> {
            String key = (String) k;
            if (!targetConnectionFactories.containsKey(key)) {
                targetConnectionFactories.put(key, connectionFactoryBuilder.buildConnectionFactory(key));
            }
            return targetConnectionFactories.get(key);
        });
    }
    

    请注意,我们在DatabaseMigrationService中使用Flyway为我们获得的每个租户创建和迁移模式。

    邰昀
    2023-03-14

    我也遇到了这个。

    以下是我目前正在做的事情:

    >

  • 将PostgresqlConnectionConfigurationBuilder和PostgresqlConnectionFactory发布为Bean:

    @Bean
    public PostgresqlConnectionConfiguration.Builder postgresqlConnectionConfiguration() {
        return PostgresqlConnectionConfiguration.builder()
                .host("localhost")
                .port(5432)
                .applicationName("team-toplist-service")
                .database("db")
                .username("user")
                .password("password");
    }
    
    @Bean
    @Override
    public PostgresqlConnectionFactory connectionFactory() {
        return new PostgresqlConnectionFactory(postgresqlConnectionConfiguration()
                .build());
    }
    

    因此,我可以稍后(在我的业务方法中)使用注入的PostgresqlConnectionConfigurationBuilder实例创建一个新的PostgresqlConnectionFactory,但现在也可以使用在生成器上调用的“schema”setter(在从传入的org.springframework.web.reactive.function.server.ServerRequest中提取租户信息后,我从路由bean传递了该请求。

    我的db模式遵循appname\u tenantId模式,因此我们将“appname”静态配置为ie“app\u name”,因此我最终使用类似“app\u name\u foo\u bar123”的模式名称

    接下来,我们有一个租户标识符,在我的例子中,它来自一个请求标头,该标头保证由位于上游的apache服务器设置(为传入请求传递一个X-Trenter-Id标头,以便不依赖URL进行租户特定路由)

    所以我的“逻辑”现在看起来有点像这样:

    public Flux<TopTeam> getTopTeams(ServerRequest request) {
    
        List<String> tenantHeader = request.headers().header("X-Tenant-Id");
        // resolve relevant schema name on the fly
        String schema = (appName+ "_" + tenantHeader.iterator().next()).replace("-", "_");
        System.out.println("Using schema: " + schema);
        // configure connfactory with schema set on the builder
        PostgresqlConnectionFactory cf = new PostgresqlConnectionFactory(postgresqlConnectionConfiguration.schema(schema).build());
        // init new DatabaseClient with tenant specific connection
        DatabaseClient cli = DatabaseClient.create(cf);
    
    
            return cli
                    .execute("select * from top_teams ").fetch().all()
                    .flatMap(map -> {
    
                        ...
                        });
                    });
        }
    

    当然,这个逻辑可以抽象出来,但不确定放在哪里,也许可以将它移动到MethodArgumentResolver,这样我们就可以注入一个已经配置好的DatabaseClient

    ps:这只解决了使用数据库客户端时的多租户问题。我不确定如何使用R2dbcRepositories使其工作

  •  类似资料:
    • 我正在学习多租户应用程序,以及如何使用PostgreSQL的模式来实现这一点。 在研究这个主题时,我发现了一篇文章,作者描述了在多租户应用程序中使用PostgreSQL模式时的糟糕体验。主要问题是迁移性能差和数据库资源使用率高。 似乎只有一个模式(在租户之间共享表)会比每个租户有一个单独的模式带来更好的性能。但我觉得很奇怪。我的想法正好相反,因为较小表上的索引往往比较大表上的索引轻。 为什么在许多

    • 我想使用Flask SQLAlchemy构建一个多租户应用程序。官方SQLAlchemy文档建议,要使用多租户,表应该分布在每个租户的1个方案中,并在引擎级别处理不同的租户。 对我来说,维护多个方案似乎有点臃肿,我想知道,如果设计正确,使用相同表格的方法对所有租户是否可行,如果没有,为什么不可行: 那些包含租户所有记录的表有一个不可为空的列,该列指示哪一个租户“拥有”该行 我几乎找不到关于这种方法

    • 我正在将当前的应用程序迁移到多租户体系结构。由于只有一个代码库,我需要解决多个租户的问题。我使用的是单数据库、多模式的方法。每个租户将被分配一个单独的模式,其中元数据保存在默认模式中。 应用程序是用ASP构建的。NET MVC。我使用Dapper连接到我的SQL Server。我有50个函数,使用直接查询和存储过程调用数据库。当为每个租户初始化dapper时,是否有任何方法可以在不改变函数的情况下

    • 情景: 我们有一个多租户应用程序,其中每个租户都有自己的模式。有一个公共模式,其中存在一个包含每个租户记录的表。因此,有一个超级管理员可以创建租户,并将管理员分配给新创建的租户。 为了实现RBAC(基于角色的访问控制),我计划将每个角色表放入租户模式,并实现一些中间件来检查授权。在孤立的模式环境中,这是一个好的体系结构吗?

    • 我目前正试图找出为我的系统设置多租户的最佳方法。我面临的问题是,租户并不总是必须是子域,但可以作为子域的一部分进行设置,子域可以有多个租户。我似乎在网上找不到任何东西可以帮助我在Laravel 6中进行设置。 系统要求: 一台服务器可以有许多子域 系统必须设置一个数据库,该数据库将使用tenant_id来确定哪些数据属于租户。 我目前正在以以下结构将所有子域数据存储在“subdomains”表中:

    • 我必须在j2ee中开发一个多租户SaaS应用程序,从Iaas和PaaS开始实现三种云模型,我选择了openstack和openshift origin。SaaS应用程序的第一个标准是多租户,我知道有三种方法来实现它——单独的数据库——共享数据库,单独的模式——共享数据库,共享模式。我在这里迷失了方向,因为许多框架,比如ATHENA,ORM,比如hibernate,还有TOPLINK。我需要帮助了解