我们过去听到的一项功能请求是以多租户方式运行Activiti引擎,使租户的数据与其他租户的数据隔离。 当然,在某些云/ SaaS环境中,这是必须的。
几个月前,波恩大学的学生拉斐尔·吉伦(Raphael Gielen)与我接触,他正在撰写有关Activiti中多租户的硕士论文。 几周前,我们在一个共同工作的咖啡馆聚会,反弹想法,并为租户一起破解了具有数据库模式隔离的第一个原型。 很有趣 :-)。
无论如何,我们一直在完善和完善该代码,并将其提交给Activiti代码库。 让我们在下面的前两个部分中了解使用Activiti进行多租户的现有方法。 在第三部分中,我们将深入研究新的多租户多模式功能,其中包含一些实际工作的代码示例!
共享数据库多租户
Activiti已经有多租户功能了一段时间(自版本5.15起)。 所采用的方法是共享数据库 :拥有一个(或多个)Activiti引擎,并且它们都进入同一个数据库。 数据库表中的每个条目都有一个租户标识符 ,最好将其理解为该数据的一种标记。 然后,Activiti引擎和API会读取并使用该租户标识符在租户的上下文中执行其各种操作。
例如,如下图所示,两个不同的租户可以具有使用相同密钥的流程定义。 引擎和API确保没有数据混合。
这种方法的好处是部署简单,因为与设置“常规” Activiti引擎没有区别。 缺点是您必须记住使用正确的API调用(即那些考虑了租户标识符的调用)。 而且,它与任何具有共享资源的系统都存在相同的问题:租户之间总是存在对资源的竞争。 在大多数用例中,这很好,但是有些用例不能以这种方式完成,例如为某些租户提供更多或更少的系统资源。
多引擎多租户
自Activiti的第一个版本以来,另一种可能的方法是为每个租户简单地拥有一个引擎实例:
在此设置中,每个租户可以具有不同的资源配置,甚至可以在不同的物理服务器上运行。 当然,此图中的每个引擎可以是多个引擎,以提高性能/故障转移/等等。 现在的好处是,资源是为租户量身定制的。 缺点是设置比较复杂(多个数据库架构,每个租户都有不同的配置文件,等等)。 每个引擎实例都将占用内存(但是Activiti会占用很少的内存)。 另外,您无需编写一些路由组件 ,就可以以某种方式知道当前租户上下文并路由到正确的引擎。
多架构多租户
Activiti多租户故事的最新添加是在两周前添加的(这是commit ),同时是版本5和6的添加。这里,每个租户都有一个数据库(模式),但只有一个引擎实例。 再次,在实践中,可能有多个用于性能/故障转移/实例的实例,但是概念是相同的:
好处显而易见:只有一个引擎实例可以管理和配置,而且API与非多租户引擎完全相同。 但最重要的是,租户的数据与其他租户的数据完全分开。 缺点(类似于多引擎多租户方法)是有人需要管理和配置不同的数据库。 但是复杂的引擎管理已不复存在。
我上面链接到的提交还包含一个单元测试 ,该测试显示了多模式多租户引擎的工作方式。
构建流程引擎很容易,因为有一个MultiSchemaMultiTenantProcessEngineConfiguration可抽象出大多数细节:
config = new MultiSchemaMultiTenantProcessEngineConfiguration(tenantInfoHolder);config.setDatabaseType(MultiSchemaMultiTenantProcessEngineConfiguration.DATABASE_TYPE_H2);
config.setDatabaseSchemaUpdate(MultiSchemaMultiTenantProcessEngineConfiguration.DB_SCHEMA_UPDATE_DROP_CREATE);config.registerTenant("alfresco", createDataSource("jdbc:h2:mem:activiti-mt-alfresco;DB_CLOSE_DELAY=1000", "sa", ""));
config.registerTenant("acme", createDataSource("jdbc:h2:mem:activiti-mt-acme;DB_CLOSE_DELAY=1000", "sa", ""));
config.registerTenant("starkindustries", createDataSource("jdbc:h2:mem:activiti-mt-stark;DB_CLOSE_DELAY=1000", "sa", ""));processEngine = config.buildProcessEngine();
这看起来与启动常规Activiti流程引擎实例非常相似。 主要区别在于我们是使用引擎注册租户。 每个租户都需要添加其唯一的租户标识符和数据源实现。 当然,数据源实现需要有自己的连接池。 这意味着您可以根据使用情况有效地为某些租户提供不同的连接池配置。 Activiti引擎将确保已创建或验证每个数据库架构都是正确的。
魔术以使这一切工作是TenantAwareDataSource 。 这是一个javax.sql.DataSource实现,它根据当前租户标识符委托给正确的数据源。 此类的想法在很大程度上受到Spring的AbstractRoutingDataSource (站在其他开源项目的肩膀上!)的影响。
通过从TenantInfoHolder实例获取当前的租户标识符 ,可以路由到正确的数据源。 正如您在上面的代码片段中看到的那样,在构造MultiSchemaMultiTenantProcessEngineConfiguration时,这也是必需的参数。 TenantInfoHolder是您需要实现的接口,具体取决于您环境中用户和租户的管理方式。 通常,您将使用ThreadLocal来存储由某些安全过滤器填充的当前用户/租户信息(类似于Spring Security)。 该类有效地充当下图中的“路由组件”:
在单元测试示例中,我们确实使用ThreadLocal来存储当前的租户标识符 ,并用一些演示数据填充它:
private void setupTenantInfoHolder() {DummyTenantInfoHolder tenantInfoHolder = new DummyTenantInfoHolder();tenantInfoHolder.addTenant("alfresco");tenantInfoHolder.addUser("alfresco", "joram");tenantInfoHolder.addUser("alfresco", "tijs");tenantInfoHolder.addUser("alfresco", "paul");tenantInfoHolder.addUser("alfresco", "yvo");tenantInfoHolder.addTenant("acme");tenantInfoHolder.addUser("acme", "raphael");tenantInfoHolder.addUser("acme", "john");tenantInfoHolder.addTenant("starkindustries");tenantInfoHolder.addUser("starkindustries", "tony");this.tenantInfoHolder = tenantInfoHolder;}
现在,我们开始一些流程实例,同时还要切换当前的租户标识符。 在实践中,您必须想象多个线程随请求一起进入,并且它们将根据登录的用户设置当前的租户标识符:
startProcessInstances("joram");
startProcessInstances("joram");
startProcessInstances("raphael");
completeTasks("raphael");
上面的startProcessInstances方法将使用标准Activiti API设置当前用户和租户标识符,并启动几个流程实例, 就好像根本没有多租户一样 ( completeTasks方法类似地完成了一些任务)。
同样很酷的是, 您可以通过使用与构建流程引擎时相同的方法来动态注册(和删除)新的租户 。 Activiti引擎将确保已创建或验证数据库架构。
config.registerTenant("dailyplanet", createDataSource("jdbc:h2:mem:activiti-mt-daily;DB_CLOSE_DELAY=1000", "sa", ""));
这是一部电影,显示正在运行的单元测试以及有效隔离的数据:
多租户执行器
最后一个难题是工作执行者。 常规Activiti API调用“借用”当前线程以执行其操作,因此可以使用之前在该线程上设置的任何用户/租户上下文。
但是,作业执行程序使用后台线程池运行,并且没有此类上下文。 由于Activiti中的AsyncExecutor是一个接口,因此实现多模式多租户作业执行器并不难。 当前,我们添加了两个实现。 第一个实现称为SharedExecutorServiceAsyncExecutor :
config.setAsyncExecutorEnabled(true);
config.setAsyncExecutorActivate(true);
config.setAsyncExecutor(new SharedExecutorServiceAsyncExecutor(tenantInfoHolder));
此实现(顾名思义)对所有租户使用一个线程池。 每个租户确实有其自己的作业获取线程,但是一旦获取了作业,便将其放到共享线程池中。 该系统的好处是Activiti使用的线程数受到限制。
第二种实现称为ExecutorPerTenantAsyncExecutor :
config.setAsyncExecutorEnabled(true);
config.setAsyncExecutorActivate(true);
config.setAsyncExecutor(new ExecutorPerTenantAsyncExecutor(tenantInfoHolder));
顾名思义,该类充当“代理” AsyncExecutor。 对于每个注册的租户,将启动完整的默认AsyncExecutor。 每个都有自己的获取线程和执行线程池。 “代理”仅委托给正确的AsyncExecutor实例。 这种方法的好处是,每个租户都可以根据租户的需求进行细粒度的作业执行者配置。
结论
一如既往,欢迎所有反馈。 放手多方案多租户,让我们知道您的想法以及将来可以改进的地方!
翻译自: https://www.javacodegeeks.com/2015/10/multi-tenancy-with-separate-database-schemas-in-activiti.html