spring - 使用分区与动态租户进行多方案 Multi-Tenancy

标签 spring dynamic eclipselink partitioning multi-tenant

我正在编写一个必须是 Multi-Tenancy 的Web应用程序。我正在将JPA用于持久层,并且正在有兴趣地评估EclipseLink。

我要使用的 Multi-Tenancy 策略是:每个客户一个模式。 Hibernate支持这种策略(http://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html#d5e4771),并且我已经成功地使用了它。但是,AFAIK仅在使用本地Hibernate API时才支持它,而我想使用JPA。

另一方面,EclipseLink支持单表和多表 Multi-Tenancy 策略。但是,它也支持分区,并且通过简单的自定义分区策略,我可以轻松地为每个客户设置一个分区。

第一个问题可能是在此用例中使用分区是否合适。

但是,主要问题是,客户群可能(希望)随着时间的推移而增长,因此我必须使EclipseLink动态“了解”新客户(即:无需重新启动Webapp)。据我了解,要在EclipseLink中设置分区,我必须使用不同的“连接池”(或“节点”)来设置我的持久性单元:每个节点都有其配置的数据源和名称。另一方面,分区策略将通过其名称确定要使用的节点。到目前为止一切顺利,但是我计划使用Spring的LocalContainerEntityManagerFactoryBean设置我的持久性单元。在处理LocalContainerEntityManagerFactoryBean时,我可能会在启动时动态发现客户,这样我就可以在那时传递所有节点/客户的所有必需属性,但是如果之后添加新客户会怎样?我认为动态更改持久性单元属性不会对已经构造的EntityManagerFactory单例实例产生任何影响...并且我担心EclipseLink会抱怨如果我请求一个在EntityManagerFactory创建时尚不知道其对应节点的分区。如我错了请纠正我。

我认为将LocalContainerEntityManagerFactoryBean范围声明为“prototype” bean将是一个非常糟糕的主意,并且我认为它根本不起作用。另一方面,由于客户交互绑定到特定的HTTP会话,因此我可以通过将LocalContainerEntityManagerFactoryBean范围声明为“会话”来使用“中间”方法,但是我认为在这种情况下,我将不得不处理诸如增加了内存消耗,并在多个EntityManagerFactories之间共享了缓存(在给定时间使用该应用程序的每个客户一个)。

如果我无法使该策略生效,那么我认为我将不得不放弃整个分区,而只能使用“动态数据源路由”方法,但是在这种情况下,我担心的是EclipseLink共享缓存的一致性(我想我必须完全禁用它,这将是一个真正的缺点)。

预先感谢您对此的任何反馈。

最佳答案

老实说,我没有尝试克里斯的建议,而是选择了更精细的解决方案。这是我的解决方案。

在我的情况下,

  • ,租户=客户;每个客户数据都在其自己的数据库模式中,可能位于一个专用的DBMS实例中(不管是哪个供应商);换句话说,每个客户我有一个不同的数据源
  • 因为我使用分区,所以这意味着每个客户都有自己的分区;每个分区由相应的唯一客户ID
  • 标识
  • 每个登录到该应用程序的用户都属于一个不同的客户。我使用Spring Security处理身份验证和授权,因此可以通过查询SecurityContextHolder
  • 来检索有关用户(包括其拥有的客户)的信息
  • 我定义了我自己的EclipseLink PartitioningPolicy,它如上一点所述确定了当前登录用户的客户,然后返回一个列表,该列表仅包含标识该客户分区
  • Accessor
  • 我的所有表都必须分区,并且我不想在每个带有注释的实体上指定该分区,因此我在启动时将此分区策略注册到EclipseLink中,并将其设置为默认表。简要地:
    JpaEntityManagerFactory jpaEmf = entityManagerFactory.unwrap(JpaEntityManagerFactory.class);
    ServerSession serverSession = jpaEmf.getServerSession();
    serverSession.getProject().addPartitioningPolicy(myCustomerPolicy);
    serverSession.setPartitioningPolicy(myCustomerPolicy);
    

  • 然后,要将数据源动态添加到EclipseLink(在EclipseLink术语中称为“连接池”),以使上述策略指定的客户ID与EclipseLink中的已知“连接池”相匹配,请执行以下操作:
  • 侦听器会拦截任何用户成功登录
  • ,此侦听器查询EclipseLink,以查看它已经知道由用户客户ID标识的连接池;如果是这样,我们就完成了,EclipseLink可以正确处理该分区;否则,将创建一个新的连接池并将其添加到EclipseLink;概念证明:
    String customerId = principal.getCustomerId();
    JpaEntityManagerFactory jpaEmf = entityManagerFactory.unwrap(JpaEntityManagerFactory.class);
    ServerSession serverSession = jpaEmf.getServerSession();
    if (!serverSession.getConnectionPools().containsKey(customerId)) {
      DataSource customerDataSource = createDataSourceForCustomer(customerId);
      DatabaseLogin login = new DatabaseLogin();
      login.useDataSource(customerId);
      login.setConnector(new JNDIConnector(customerDataSource));
      Class<? extends DatabasePlatform> databasePlatformClass = determineDbVendorPlatform(customerId);
      login.usePlatform(databasePlatformClass.newInstance());
      ConnectionPool connectionPool = new ExternalConnectionPool(customerId, login, serverSession);
      connectionPool.startUp();
      serverSession.addConnectionPool(connectionPool);
    }
    

  • 用户登录操作当然是针对中央数据库(或任何其他身份验证源)执行的,因此以上代码在执行任何特定于客户的JPA查询之前发生(因此,在连接之前,将客户连接池添加到EclipseLink中)分区策略曾经引用过它)。

    但是,有一个重要方面需要考虑。在EclipseLink中,数据分区意味着可识别的数据(=实体实例)仅在一个分区中,或者相等地复制到多个分区中。实体实例身份是通过标识符(=主键)确定的。这意味着对于两个不同的客户/租户T1和T2,不应存在两个具有相同id = x的E类型的不同实体实例,否则EclipseLink可能会认为它们是完全相同的实体实例。这可能导致在单个JPA会话期间读取/写入来自不同客户的混合数据=>灾难。
    可能的解决方案:

    在这种情况下,
  • 使用的分区由当前登录的用户确定。这意味着在HTTP会话范围内执行的每个查询都将是相同的;由于我使用的是事务范围内的实体管理器,其寿命最多等于请求持续时间(它本身在HTTP会话中可以很好地扩展),因此仅禁用EclipseLink共享缓存就可以避免来自不同客户的数据混合的可能性。但是,这仍然是不可取的
  • 我能找到的最好的选择是确保所有ID(=主键)都已生成,并且该生成由EclipseLink以集中的跨客户方式处理,因此,实体E的id = x肯定分配给了仅一个客户的一个实体实例;这实际上意味着对客户进行ID分配序列的“分区”,并阻止使用MySQL自动增量列(又名数据库标识生成类型);所以我选择使用表生成类型作为实体标识符,并将该表放入存储用户和客户信息的中央数据库中

  • 要正确实现选项2,要解决的最后一个小问题是,即使EclipseLink文档说可以使用eclipselink.connection-pool.sequence配置选项指定专用于表排序的连接池(=数据源),当如上所述设置默认分区策略。实际上,我的客户参与策略会针对每个查询(甚至那些用于ID分配的查询)都被调用。因此,策略必须拦截这些查询并将其路由到中央数据源。
    我找不到解决此问题的最终解决方案,但我能想到的最佳选择是:
  • 如果查询的SQL字符串以“UPDATE SEQUENCE”开头,则表示它是对id分配的查询,并假设专用于序列分配的表称为SEQUENCE(这是默认设置)
  • (如果您采用约定为生成器添加SEQUENCE后缀),如果执行的查询名称以“SEQUENCE”结尾,则表示它是对ID分配的查询

  • 我通过正确定义我的id生成映射选择了选项2:
    @Entity
    public class MyEntity {
      @Id
      @TableGenerator(name = "MyEntity_SEQUENCE", allocationSize = 10)
      @GeneratedValue(generator = "MyEntity_SEQUENCE")
      private Long id;
    }
    

    这使EclipseLink使用名为SEQUENCE的表,该表包含一行SEQ_NAME列值为MyEntity_SEQUENCE的行。用于更新此ID分配序列的查询将被命名为MyEntity_SEQUENCE,我们已经完成。
    但是,我已将我的参与策略设置为可配置的,以便在EclipseLink实现中发生某些更改而打破了这种“启发式”方法时,可以随时从一种顺序查询标识策略切换到另一种。

    这基本上是整个图片。目前,它运行良好。
    欢迎提供反馈,改进,建议。

    关于spring - 使用分区与动态租户进行多方案 Multi-Tenancy ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26863928/

    相关文章:

    mysql - 如何在 MySQL 存储过程中使用动态 SQL

    jpa - OneToOne 关系上的 EclipseLink PreUpdate 未持久化

    jaxb - EclipseLink MOXy - 如何避免将 jaxb.in​​dex 和 jaxb.properties 文件保留在模型类包中?

    java - 构造函数中的 Spring Data JPA JPQL 列表

    java - Spring MVC - 从另一个休息服务内部调用休息服务

    dynamic - Vue 如何将动态 id 与来自 v-for 循环 + 字符串的字段连接?

    asp.net - 为什么 ASP.NET 动态控件即使添加到 Page_Load 中也能保持 ViewState?

    java - 我可以让 MOXy 在生成 json 时重命名元素吗?

    spring - Spring Reactive Webclient 的请求级背压?

    java - Spring Integration 解码变压器 Jaxb2Marshaller 性能问题