使用分区与动态租户进行多模式多租户

时间:2014-11-11 11:34:52

标签: spring dynamic eclipselink partitioning multi-tenant

我正在编写一个必须是多租户的Web应用程序。我正在使用JPA作为持久层,我正在兴趣地评估EclipseLink。

我想要使用的多租户策略是:每个客户一个架构。 Hibernate支持这样的策略(http://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html#d5e4771),我已经成功地使用了它。但是,AFAIK只有在使用本机Hibernate API时才支持它,而我想使用JPA。

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

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

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

我认为将EntityManagerFactory范围声明为“原型”bean将是一个非常糟糕的主意,我认为它根本不起作用。另一方面,由于客户交互绑定到特定的HTTP会话,我可以通过将LocalContainerEntityManagerFactoryBean范围声明为“会话”来使用“中间”方法,但我认为在这种情况下我会有管理多个LocalContainerEntityManagerFactoryBean之间增加的内存消耗和共享缓存协调等问题(在给定时间使用该应用程序的每个客户一个)。

如果我不能使这个策略工作,我想我将不得不放弃整个分区并回归到“动态数据源路由”方法,但在这种情况下我关注EclipseLink共享缓存一致性(我想我必须完全禁用它,这将是一个真正的缺点)。

提前感谢您对此的任何反馈。

2 个答案:

答案 0 :(得分:1)

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

  • 在我的情况下,租户=客户;每个客户数据都在自己的数据库模式中,可能位于专用的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(它们被称为#34;连接池"在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 和<不应存在两个不同的 E 类型的实体实例,其id = x em> T2 ,否则EclipseLink可能会认为它们是完全相同的实体实例。这可能导致在单个JPA会话期间读取/写入来自不同客户的混合数据=&gt;灾难。 可能的解决方案:

  1. 在这种情况下,要使用的分区由当前登录的用户确定;这意味着对于在HTTP会话范围内执行的每个查询,它都是相同的;因为我使用事务范围的实体管理器,其生命周期最多等于请求持续时间(它本身在HTTP会话中很好地延伸),所以只需禁用EclipseLink共享缓存就可以避免混合来自不同客户的数据;然而,这仍然是不受欢迎的
  2. 我能找到的最佳选择是确保生成所有ID(=主键),并且EclipseLink以中央跨客户方式处理生成,因此id = x 对于实体 E 当然只分配给一个客户的一个实体实例;这实际上意味着&#34;分区&#34;客户端的id赋值序列,并阻止使用MySQL自动增量列(也称为数据库标识生成类型);所以我选择将表生成类型用于实体标识符,并将该表放在存储用户和客户信息的中央数据库中
  3. 正确实现选项2要解决的最后一个小问题是,即使EclipseLink文档说可以使用{{1}指定专用于表排序的连接池(=数据源)配置选项,当如上所述设置默认分区策略时,这似乎被忽略。实际上,我的客户partitioining策略会被调用用于每个查询,甚至是那些用于id分配的查询。因此,策略必须拦截这些查询并将它们路由到中央数据源。 我无法找到解决这个问题的最终解决方案,但我能想到的最佳选择是:

    1. 如果查询的SQL字符串以&#34; UPDATE SEQUENCE&#34;开头,则表示它是id分配的查询,假设专用于序列分配的表称为SEQUENCE(这是默认的)
    2. 如果您采用约定为生成器添加SEQUENCE后缀,如果执行的查询名称以&#34; SEQUENCE&#34;结尾,则表示它是id分配的查询
    3. 我通过正确定义我的id生成映射选择了选项2:

      eclipselink.connection-pool.sequence

      这使得EclipseLink使用名为@Entity public class MyEntity { @Id @TableGenerator(name = "MyEntity_SEQUENCE", allocationSize = 10) @GeneratedValue(generator = "MyEntity_SEQUENCE") private Long id; } 的表,其中包含SEQUENCE列值为SEQ_NAME的一行。用于更新此序列以进行ID分配的查询将命名为MyEntity_SEQUENCE,我们已完成。 但是我使我的partitioining策略可配置,以便我可以随时从一个序列查询识别策略切换到另一个序列查询识别策略,以防EclipseLink实现中的某些更改打破了这个&#34;启发式&#34;。

      这基本上就是全貌。目前,它一直运作良好。 欢迎提供反馈,改进和建议。

答案 1 :(得分:0)

在这里描述的EclipseLink EntityManagerFactory类上查看refreshMetadata:http://wiki.eclipse.org/EclipseLink/DesignDocs/340192#EntityManagerFactory这将导致单例重新加载配置数据。它不会影响正在运行的EntityManager实例,但会导致获得的任何新EntityManagers使用新的配置数据,这似乎符合您的用法。

需要解包EntityManagerFactory才能访问http://javadox.com/org.eclipse.persistence/eclipselink/2.5.0/org/eclipse/persistence/jpa/JpaEntityManagerFactory.html界面:

JpaHelper.getEntityManagerFactory(em).refreshMetadata(properties);