我们通常会构建需要一起执行以下几项操作的应用程序:调用后端(微)服务,写入数据库,发送JMS消息等。但是,如果在调用其中之一时出错,会发生什么情况?远程资源,例如,在调用Web服务后,如果数据库插入失败? 如果远程服务调用写入数据,则由于服务已提交其数据,但尚未提交对数据库的调用,您可能会陷入全局不一致的状态。 在这种情况下,您将需要补偿错误,通常,补偿的管理是复杂且需要手写的。
红帽公司的Arun Gupta在 DZone微服务Refcard入门 。 实际上,这些模式中的大多数都显示了调用多个其他微服务的微服务。 在所有这些情况下,全局数据一致性变得很重要,即确保补偿后一个对微服务的调用中的故障或者得到补偿,或者重新尝试执行该调用,直到所有微服务中的所有数据再次保持一致为止。 在其他有关微服务的文章中,通常很少或根本没有提到跨远程边界的数据一致性,例如,标题为“微服务不是免费的午餐 ”的好文章, 当事情必须发生时 ,作者只是用陈述来触及问题。 ……在交易上……事情变得复杂,我们需要管理……分布式交易以将各种行动联系在一起
在分布式环境中管理一致性的传统方法是利用分布式事务。 部署了事务管理器来监督全局系统是否保持一致。 已经开发了诸如两阶段提交的协议来标准化过程。 JTA,JDBC和JMS是使应用程序开发人员能够保持多个数据库和消息服务器一致的规范。 JCA是允许开发人员围绕企业信息系统(EIS)编写包装的规范。 在最近的一篇文章中,我写了关于如何构建通用JCA连接器的信息,该连接器使您可以将对微服务的调用等绑定到这些全局分布式事务中,从而使您不必编写自己的框架代码来处理期间的故障。分布式交易。 连接器负责确保您的数据最终保持一致
但是,您将永远无法访问支持JCA的完整Java EE应用服务器,尤其是在微服务环境中。因此,我现在扩展了该库,使其在以下环境中包括对提交/回滚/恢复的自动处理:
- Spring靴
- Spring+ Tomcat /码头
- Servlet + Tomcat /码头
- Spring批
- 独立的Java应用程序
为了做到这一点,应用程序需要使用与JTA兼容的事务管理器,即Atomikos或Bitronix之一 。
以下描述基于您已经阅读了较早的博客文章这一事实。
设置远程调用以使其参与事务的过程类似于使用先前博客文章中提供的JCA适配器时的过程。 有两个步骤:1)在传递给从BasicTransactionAssistanceFactory类检索的TransactionAssistant对象的回调中调用远程服务,以及2)设置中央提交/回滚处理程序。
执行阶段的代码(请参见前面的博客文章),如下所示(使用Spring时):
@Service
@Transactional
public class SomeService {
@Autowired @Qualifier("xa/bookingService")
BasicTransactionAssistanceFactory bookingServiceFactory;
public String doSomethingInAGlobalTransactionWithARemoteService(String username) throws Exception {
//write to say a local database...
//call a remote service
String msResponse = null;
try(TransactionAssistant transactionAssistant = bookingServiceFactory.getTransactionAssistant()){
msResponse = transactionAssistant.executeInActiveTransaction(txid->{
BookingSystem service = new BookingSystemWebServiceService().getBookingSystemPort();
return service.reserveTickets(txid, username);
});
}
return msResponse;
}
}
清单1:在事务内调用Web服务
第5-6行提供了第13行用于获取TransactionAssistant的工厂实例。 注意,您必须确保此处使用的名称与下面清单3中的安装过程中使用的名称相同。 这是因为当事务被提交或回滚时,事务管理器需要找到用于提交或补偿第16行进行的调用的相关回调。很有可能在应用程序中将有多个这样的远程调用,对于集成的每个远程服务,必须编写清单1中所示的代码。请注意,此代码与使用JDBC调用数据库没有什么不同。 对于登记到事务中的每个数据库,您需要:
- 注入数据源(类似于第5-6行)
- 从数据源获得连接(第13行)
- 创建一条语句(第14行)
- 执行语句(第15-16行)
- 完交易助手之后,在交易完成之前关闭它非常重要
为了创建BasicTransactionAssistanceFactory的实例(清单1中的第5-6行),我们使用Spring @Configuration :
@Configuration
public class Config {
@Bean(name="xa/bookingService")
public BasicTransactionAssistanceFactory bookingSystemFactory() throws NamingException {
Context ctx = new BitronixContext();
BasicTransactionAssistanceFactory microserviceFactory =
(BasicTransactionAssistanceFactory) ctx.lookup("xa/bookingService");
return microserviceFactory;
}
...
清单2:Spring的
清单2的第4行使用的名称与清单1的第5行的@Qualifier相同。清单2的第5行的方法通过在JNDI中查找工厂来创建工厂,在本示例中使用Bitronix。 使用Atomikos时,代码看起来略有不同-有关详细信息,请参见demo/genericconnector-demo-springboot-atomikos项目。
上面提到的第二步是设置提交/回滚回调。 当提交或回滚清单1第8-20行的事务时,事务管理器将使用此方法。 请注意,由于清单1第2行上有@Transactional批注,因此存在事务。清单3中显示了此设置:
CommitRollbackCallback bookingCommitRollbackCallback = new CommitRollbackCallback() {
private static final long serialVersionUID = 1L;
@Override
public void rollback(String txid) throws Exception {
new BookingSystemWebServiceService().getBookingSystemPort().cancelTickets(txid);
}
@Override
public void commit(String txid) throws Exception {
new BookingSystemWebServiceService().getBookingSystemPort().bookTickets(txid);
}
};
TransactionConfigurator.setup("xa/bookingService", bookingCommitRollbackCallback);
清单3:设置一个提交/回滚处理程序
第12行将回调与配置清单1和2中使用的唯一名称一起传递给配置器。
如果要集成的服务仅提供执行方法和该执行的补偿方法,则第9行的提交很可能为空。 此提交回调来自两个阶段的提交,其目的是将分布式系统不一致的时间保持在绝对最小限度。 请参阅本文末尾的讨论。
应该是无状态的
在此示例中,传递给第4行和第8行的回调的事务ID(名为txid的字符串)被传递给Web服务。 在一个更实际的示例中,您将使用该ID查找在执行阶段保存的上下文信息(请参见清单1的第15和16行)。 然后,您将使用该上下文信息,例如来自对Web服务的较早调用的参考号,来进行提交或回滚清单1中的Web服务调用的调用。
这些清单的独立变体(例如在Spring环境之外使用此库)几乎相同,不同之处在于您需要手动管理事务。 有关某些受支持环境中的代码示例,请参见Github上的demo文件夹。
请注意,在通用连接器的JCA版本中,您可以配置通用连接器是否在内部处理恢复。 如果没有,则必须提供事务管理器可以调用的回调,以查找您认为尚未完成的事务。 在本文讨论的非JCA实现中,这始终由通用连接器在内部进行处理。 通用连接器会将上下文信息写入目录,并在恢复期间使用该信息来告诉事务管理器需要清除哪些内容。 严格来说,这不是很正确,因为如果您的硬盘发生故障,所有有关不完整事务的信息都将丢失。 在严格的两阶段提交中,这就是为什么允许事务管理器调用资源以获取需要恢复的不完整事务的列表的原因。 在当今的RAID控制器世界中,没有理由使生产机器由于硬盘故障而丢失数据,因此,目前没有选择提供对通用连接器的回调,该回调可以告诉它正在进行的事务。需要恢复的状态。 如果节点发生灾难性的硬件故障(无法启动该节点并再次运行),则需要将通用连接器写入的所有文件从旧硬盘上物理复制到第二个硬盘上。节点。 然后,在第二个节点上运行的事务管理器和通用连接器将通过提交或回滚这些崩溃的事务中的任何一个来协调工作,以完成所有挂起的事务。 此过程与灾难恢复期间复制事务管理器日志没有什么不同,具体取决于您使用的是哪个事务管理器。 您执行此操作的机会非常小-在我的职业生涯中,我从未听说过我所从事的项目/产品中的生产机器以这种方式失败。
您可以使用清单4所示的第二个参数来配置此上下文信息的写入位置:
MicroserviceXAResource.configure(30000L, new File("."));
显示的值也是默认值。
清单4设置了与恢复相关的最小事务寿命。 在这种情况下,只有在30秒钟以上时,该事务才被认为与通过恢复进行清理相关。 您可能需要根据执行业务流程所需的时间来调整此值,并且可能取决于为您调用的每个后端服务配置的超时时间之和。 低值和高值之间需要权衡:值越低,失败后恢复期间在事务管理器中运行的后台任务清理所需的时间越少。 这意味着值越小,不一致窗口越小。 但是请注意,如果该值太低,恢复任务将尝试回滚实际上仍处于活动状态的事务。 通常,您可以配置事务管理器的超时期限,清单4中设置的值应大于等于事务管理器的超时期限。 此外,清单4中将存储上下文数据的目录配置为本地目录。 您可以指定任何目录,但是请确保该目录存在,因为通用连接器不会尝试创建该目录。
如果在Tomcat环境中使用Bitronix,则可能会发现关于如何配置环境的信息不多。 在Bitronix从codehaus.org移至Github之前,它的记录非常好。 我为Bitronix创建了一个问题来改进文档。 demo/genericconnector-demo-tomcat-bitronix文件夹中的源代码和自述文件包含提示和链接。
使用通用连接器要注意的最后一件事是提交和回滚的工作方式。 连接器所做的全部工作都是在JTA事务之上进行搭载,以便在某些情况需要回滚时,它通过回调获得通知。 然后,通用连接器将此信息传递到清单3中注册的回调中的代码中。在后端中实际回滚数据并不是通用连接器所做的事情–它只是调用回调,以便您可以告诉后端系统回滚数据。 通常,您不会像这样进行回滚,而是通常使用状态来标记已写入的数据不再有效。 正确回滚执行阶段已写入的所有数据痕迹可能非常困难。 在严格的两阶段提交协议设置中(例如,使用两个数据库),在执行和提交/回滚之间,写入每个资源中的数据将保持锁定状态,第三方事务不可触摸。 实际上,这是两阶段提交的缺点之一,因为锁定资源会降低可伸缩性。 通常,您集成的后端系统不会在执行阶段和提交阶段之间锁定数据,并且确实,提交回调将保持为空,因为它无关紧要–数据通常在第16行时已在后端中提交清单1中的代码在执行阶段返回。 但是,如果您想构建一个更严格的系统,并且可以影响要集成的后端的实现,那么通常可以通过使用状态将后端系统中的数据“锁定”在执行和提交阶段之间,例如,执行后为“预订机票”,提交后为“预订机票”。 第三方交易将不允许在“保留”状态下访问资源/票证。
- 通用连接器和许多演示项目可从https://github.com/maxant/genericconnector/获得 ,二进制文件和源代码可从Maven获得 。
翻译自: https://www.javacodegeeks.com/2015/10/global-data-consistency-transactions-microservices-and-spring-boot-tomcat-jetty.html