微服务架构中的全局数据一致性

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 63w+ 字,讲解图 2808+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2200+ 小伙伴加入学习 ,欢迎点击围观

我已经在 Github 上发布了一个通用的 JCA 资源适配器,可从 Maven ( ch.maxant:genericconnector-rar ) 获得,并具有 Apache 2.0 许可证 。这使您可以将 REST 和 SOAP Web 服务之类的东西绑定到 JTA 事务中,这些事务在 Java EE 应用程序服务器的控制下。这使得构建保证数据一致性的系统成为可能,并且可以非常轻松地使用尽可能少的样板代码。请务必阅读 常见问题解答

想象一下以下场景......

功能要求

  • ...许多页的睡眠诱导要求...
  • FR-4053:当用户单击“购买”按钮时,系统应预订机票,与收单方进行确认的付款交易,并向客户发送一封信,其中包括打印的机票和收据。
  • ...更多要求...

选定的非功能性需求

  • NFR-08:必须使用 NBS(公司“Nouvelle 预订系统”)预订机票,NBS 是一种部署在内部网中的 HTTP SOAP Web 服务。
  • NFR-19:输出管理(打印和发送信件)必须使用 COMS 完成,COMS 是一种同样部署在内部网中的 JSON/REST 服务。
  • NFR-22:付款必须使用我们合作伙伴的 MMF(快速赚钱)系统完成,部署在互联网上并使用 VPN 连接。
  • NFR-34:系统必须将销售订单记录写入自己的 Oracle 数据库。
  • NFR-45:与单个销售订单相关的数据必须在整个系统、NBS、COMS 和 MMF 中保持一致。
  • NFR-84:系统必须使用 Java EE 6 来实现,以便它可以部署到我们的集群应用服务器环境中。
  • NFR-99:由于 MIGRATION'16 项目,系统必须构建为可移植到不同的应用程序服务器。

分析

NFR-45 很有趣。我们需要确保跨多个系统的数据保持一致,即即使在软件/硬件崩溃期间也是如此。然而 NFR-08、NFR-19、NFR-22 和 NFR-34 让事情变得更难了。 SOAP 和 REST 不支持事务! - 不,这不完全正确。我们可以很容易地使用 JBoss 中支持 WS-AT 的 Arjuna 事务管理器之类的东西。例如,请参阅 此项目 (或其在 Github 上的源代码) 此 JBoss 示例 此 Metro 示例 。不过,这些解决方案存在几个问题:NFR-99(使用的 API 不可移植); NFR-19(REST 不支持 WS-AT,尽管 JBoss 正在筹备中 );事实上,我们正在集成的 Web 服务甚至可能不支持 WS-AT。我过去集成了许多内部和外部 Web 服务,但从未遇到过支持 WS-AT 的服务。

多年来,我从事过具有相似需求但产生不同解决方案的项目。我见过和听说过最终有效地构建自己的事务管理器的公司,这些管理器将 Web 服务绑定到事务中。我也遇到过不担心一致性而忽略 NFR-45 的公司。我喜欢一致性的想法,但我不喜欢商业项目编写一个框架来跟踪事务状态并手动提交或回滚它们以试图与 Java EE 事务管理器保持同步的想法。所以几年前,我想到了如何满足所有这些要求,同时又避免了类似于构建事务管理器的复杂解决方案。 NFR-84 几乎可以拯救,因为 Java EE 应用程序服务器支持分布式事务。我写“几乎”是因为缺少某种形式的适配器,用于将非标准资源(如 Web 服务)绑定到此类事务中。但是 Java EE 规范还包含 JSR-112 ,即用于构建资源适配器的 JCA 规范,可以绑定到分布式事务中。我的想法是构建一个通用资源适配器,可用于将 Web 服务和其他事物绑定到应用程序服务器控制下的事务中,尽可能少地进行必要的配置,并使用我可以设计的尽可能简单的 API。

分布式事务的背景

为了更好地理解这个想法,让我们看一下分布式事务和 两阶段提交 ,它们可用于将对数据库的调用绑定到 使用 SQL 的 XA 事务中。清单 1 显示了在 XA 事务中提交数据所需的语句列表:


 mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person; +-----+-------------------------------+ | id | name | +-----+-------------------------------+ | 771 | ant | +-----+-------------------------------+

清单 1:SQL 中的 XA 事务

全局事务的分支在第 1 行的数据库(资源管理器)内启动。可以使用任意事务 ID,通常应用程序服务器内的全局事务管理器生成此 ID。第 3 行是执行“业务代码”的地方,即与我们使用数据库的原因相关的所有语句,即插入数据和运行查询。一旦所有业务内容完成,并且在第 1 行和第 5 行之间您可以调用其他远程资源,事务将使用第 5 行结束。但是请注意事务尚未完成,它只是移动到全局状态事务管理器可以开始查询每个资源管理器是否应该继续并提交事务。如果只有一个资源管理器决定它不想提交数据,那么事务管理器将告诉所有其他资源管理器回滚他们的事务。然而,如果所有资源管理器都报告他们很高兴提交事务,并且他们通过对第 7 行的响应来这样做,那么事务管理器将告诉所有资源管理器使用类似在线的命令提交他们的本地事务9. 第 9 行运行后,如第 11 行的 select 语句所示,数据可供所有人使用。

两阶段提交是关于分布式环境中的一致性。除了查看快乐流程之外,我们还需要了解在执行上述每个命令后失败期间会发生什么。如果包括 prepare 语句在内的任何语句失败,则全局事务将被回滚。资源管理器和事务管理器都应该将它们的状态写入持久持久日志,以便在它们重新启动时它们可以继续该过程并确保一致性。直到并包括 prepare 语句,如果事务失败并重新启动,资源管理器将回滚事务。

如果一些资源管理器报告他们准备提交但其他人报告他们想要回滚,或者实际上其他人没有回答,那么事务将被回滚。如果资源管理器崩溃并变得不可用,这可能需要一些时间,但全局事务管理器将确保所有资源管理器回滚。

然而,一旦所有资源管理器都成功报告他们想要提交,就没有回头路了。事务管理器将尝试在所有资源管理器上提交事务,即使它们暂时不可用。结果是暂时可能会出现数据不一致的情况,其他事务可以看到,比如一个崩溃的资源管理器还没有提交,即使它已经重新启动并再次可用,但最终,数据会变得一致.这一点很重要,因为我经常听到,甚至曾经引用过两阶段协议保证 ACID 一致性。它不会——它保证了最终的一致性——只有被视为个人的本地交易才具有 ACID 属性。

两阶段提交协议中还有一个更重要的步骤,即恢复,必须针对失败情况实施。当事务管理器或资源管理器变得不可用时,事务管理器的工作是继续尝试,直到最终整个系统再次变得一致。为了做到这一点,它可以查询资源管理器以找到资源管理器认为不完整的事务。在 Mysql 中,相关命令及其结果如清单 2 所示,即事务未完成。我在清单 1 中的提交命令之前运行了这个命令。提交之后,结果集为空,因为资源管理器会自行清理并删除成功的事务。


 mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person; +-----+-------------------------------+ | id | name | +-----+-------------------------------+ | 771 | ant | +-----+-------------------------------+

清单 2:SQL 中的 XA 恢复命令

设计

JCA 规范包括事务管理器从适配器检索 XA 资源的能力,它表示理解诸如开始、结束、准备、提交、回滚和恢复等命令的资源。挑战在于利用这一事实来创建一个资源适配器,该适配器可以调用具有提交和回滚能力的服务,类似于远程数据库引擎。如果我们做一些假设,我们就可以定义一个简化的合约,这样的服务需要实现,这样我们就可以将它们绑定到分布式事务中。

考虑例如 NFR-22 和 MMF,收单系统。通常,支付系统让您先预订钱,然后不久之后再进行预订。这个想法是你打电话给他们的服务以确保有可用的资金并预留一些钱,然后你完成你这边的所有业务交易,然后在你的数据提交后一定要预订预留的钱(请参阅 常见问题解答 以了解替代方案).预订和绝对预订应该不会超过几秒钟。在发生崩溃时保留和释放保留应该不会超过几分钟。根据我的经验,机票预订系统也经常如此,在您愿意承担费用的情况下,可以预订机票,并在不久之后确认。我将初始阶段称为 执行 ,将后期称为 提交 。当然,如果您无法在您这边完成业务,则可以运行第二阶段的替代方案,即 回滚 ,以取消预订。如果您不够友好回滚并且您只是让预订保持打开状态,供应商最终应该让预订 超时 ,以便可以在其他地方使用保留的“资源”(本例中的金钱或门票),例如,以便客户可以去逛街,也可以买票给别人。

我们希望将这种系统绑定到我们的交易中。此类服务的合同如下所示:

  1. 提供者应该提供三种操作:执行、提交和回滚(尽管提交实际上是可选的 1 ),
  2. 提供者可以让未提交和非回滚的执行超时,之后任何保留的资源都可以用于其他事务,
  3. 成功执行保证事务管理器被允许提交或回滚保留的资源,只要没有发生超时 2
  4. 提交或回滚保留的调用可以多次执行而不会产生副作用(这里考虑幂等性),以便事务管理器可以在初始尝试失败时完成事务。

脚注 #1:有时 Web 服务会提供执行操作和取消调用的操作,例如,这样钱确实不会从客户帐户中扣除。但是他们不提供提交执行的操作。如果我们回到围绕清单 2 的讨论,我在其中声明交易是最终一致的而不是立即一致的,那么很明显,全局交易中的系统是否在执行阶段肯定预订资源而不是等待并不重要直到提交阶段。最终,要么所有系统也都提交,要么全部回滚,货币交易将被取消,从而释放客户账户上的保留资金。但是请注意,提供所有三种操作的服务更简洁,如果可能影响系统设计,建议确保集成的服务提供所有三种操作:执行、提交和回滚。

脚注 #2:成功调用执行操作后,Web 服务可能不会因为业务规则而拒绝提交或回滚事务。它可能只是由于技术问题而暂时失败,在这种情况下,事务管理器可能会在不久之后再次尝试完成。将业务规则构建到提交或回滚操作中是不可接受的。所有验证必须在执行期间完成,即在提交或回滚时间之前。在数据库世界中也是如此——在 XA 事务期间,数据库必须最迟在准备阶段检查所有约束,即肯定在提交或回滚阶段之前。

让我们将使用这样的合同与使用数据库进行比较。以收单方 Web 服务为例:在执行期间保留的资金实际上被放在一边,并且不再可供尝试创建信用卡交易的其他实体使用。但是钱也没有转到我们的账户。存在三种状态: i) 钱在客户的贷方; ii) 这笔钱是保留的,不得用于其他交易; iii) 这笔钱已被预订,客户不再可用。这类似于数据库事务: i) 尚未插入一行; ii) 该行已插入,但当前对其他事务不可见(尽管这取决于事务隔离级别); iii) 最后一行被提交并且对所有人可见。尽管这类似于收单方示例,但一旦提交执行阶段,在 Web 服务中保留资金的交易就立即对全世界可见 - 直到提交阶段之后,它才保持不可见状态,就像数据库的情况一样。隔离级别不同,但当然可以构建 Web 服务来隐藏此类信息,例如基于状态,如果需要的话。

对于这样的合同,WS-AT 和两阶段提交的设计方式存在几个根本差异。首先,封装在 Web 服务中的事务在执行和提交/回滚之间不会保持打开状态。其次,因为事务没有保持打开状态,所以资源没有被锁定,就像在使用数据库时那样。这两个差异导致了第三个差异:回滚 Web 服务调用通常不是撤销它所做的事情,而是改变它所做事情的状态,以便从业务角度来看,资源再次可用。

这些差异使通用连接器比传统的两阶段提交更具优势。在这个连接器中真正发生的事情是我们搭载分布式事务,挑选最好的部分,即执行、提交、回滚和恢复。通过在 Java EE 应用服务器中执行此操作,我们可以免费获得事务管理!

最后一个阶段,即 恢复 (参见清单 1 和 2)是必需的,但它不一定需要由 Web 服务实现,因为适配器可以在内部处理该部分——毕竟它知道事务的状态,因为它一直在调用网络服务。

因此,根据上述假设,我们可以构建一个通用的 JCA 资源适配器,它跟踪事务状态并在正确的时间调用 Web 服务上的提交/回滚操作,当事务管理器告诉 XA 资源执行诸如启动事务之类的操作时,执行一些业务代码并提交或回滚事务。

适用于微服务架构

与单体应用程序相比, 微服务架构 或 SOA 有一个值得注意的问题,即很难在分布式系统中保持数据一致。微服务通常会提供执行工作的操作,但也应该提供取消该工作的操作。工作不需要做成不可见的,但就业务而言确实需要取消,这样就没有更多的资源(金钱、时间、人力等)投入到工作中。此处介绍的适配器可以在“应用程序层”内部使用,即您的客户端(移动、富 Web 客户端等)调用的服务。该层应该部署在 Java EE 应用程序服务器中,并在每次调用您的环境中的一个微服务时使用通用连接器。这样,如果出现故障,事务管理器可以“回滚”所有微服务调用。应用层的重点是控制全局事务,以便事务管理器可以监控和协调任何需要一致完成的事情,而不是说直接从客户端调用每个微服务,然后必须编写清理代码并恢复一致性。

使用适配器

我给自己的第一个要求是构建一个 API,允许您在现有事务中向 Web 服务添加业务调用。清单 3 显示了一个示例,说明如何使用 Java 8 lambda 将 Web 服务调用绑定到一个事务中(即使 API 与 Java 1.6 兼容——参见 Github 示例)。


 mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person; +-----+-------------------------------+ | id | name | +-----+-------------------------------+ | 771 | ant | +-----+-------------------------------+

清单 3:将 Web 服务调用绑定到事务中

第 1 行将该类指定为一个 EJB,它默认使用容器管理的事务,并且要求在每个方法调用时都存在一个事务,如果不存在则启动一个。第 4-5 行确保将资源适配器相关类的实例注入到服务中。第 9 行创建一个新的 Web 服务客户端实例。此客户端代码是使用 wsimport 和 WSDL 服务定义生成的。第 13-14 行创建资源适配器可用的“事务助手”。然后在第 21 行使用助手在事务中运行第 23 行。在幕后,这会设置事务管理器用来提交或回滚连接的 XA 资源。第 23 行返回一个 String ,它同步设置第 20 行的 String

将此代码与写入数据库进行比较:第 4 行和第 5 行就像注入 DataSource EntityManager ;第 9 行和第 13-14 行类似于打开与数据库的连接;最后第 21-23 行就像调用执行一些 SQL 一样。

第 23 行没有做任何错误处理。如果 Web 服务抛出异常,则会导致事务被回滚。如果您决定捕获这样的异常,您需要记住要么抛出另一个异常,以便容器回滚事务,要么您需要通过在会话上下文中调用 setRollbackOnly() 来设置事务回滚(演示代码在Github 显示了一个捕获 SQLException 示例)。

因此,将 Web 服务调用绑定到事务中的开销非常小,类似于在数据库上执行一些 SQL。重要的是,提交或回滚在上面的应用程序代码中是不可见的。然而,我们仍然需要向应用服务器展示如何提交和回滚。每个 Web 服务仅执行一次,如清单 4 所示。


 mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person; +-----+-------------------------------+ | id | name | +-----+-------------------------------+ | 771 | ant | +-----+-------------------------------+

清单 4:一次性注册回调以处理提交和回滚

此处,第 1-2 行告诉应用程序服务器创建一个单例并在应用程序启动后立即创建。这很重要,因此如果资源适配器需要恢复可能未完成的事务,它可以在准备就绪后立即这样做。第 5-6 行与清单 3 中的类似。第 11 行是我们向资源适配器注册回调的地方,以便它了解如何在 Web 服务中提交和回滚事务。我在这里也使用了 Java 8 lambdas,但是如果你使用的是 Java 6/7,你可以使用匿名内部类而不是第 11 行的新构建器。第 13-14 行只是调用网络服务来预订以前的票reserved,在清单 3 的第 23 行。如果事务管理器决定回滚全局事务,第 17-18 行取消保留的票证。非常重要的是,第 26 行在应用程序关闭时取消注册适配器实例的回调。这是必要的,因为适配器只允许您为每个 JNDI 名称(Web 服务)注册一个回调,如果应用程序在没有取消注册回调的情况下重新启动,第 11 行将失败并在第二次注册回调时出现异常。

如您所见,使用我创建的通用适配器,将 Web 服务或实际上不支持事务的任何事物绑定到 JTA 全局事务中非常容易。唯一剩下的就是配置适配器,以便它可以与您的应用程序一起部署。

适配器配置

适配器需要为每个 Web 服务配置一次,它应该绑定到事务中。为了更清楚地说明这一点,请考虑清单 4 中用于注册提交和回滚回调的代码。每个适配器 实例 只能注册一个回调,即 JNDI 名称。配置适配器是特定于应用程序服务器的,但这只是因为您放置以下 XML 的位置。在 Jboss EAP 6 / Wildfly 8 以上,它被放入 <jboss-install-folder>/standalone/configuration/standalone.xml ,在类似于 <subsystem xmlns="urn:jboss:domain:resource-adapters:...>


 mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person; +-----+-------------------------------+ | id | name | +-----+-------------------------------+ | 771 | ant | +-----+-------------------------------+

清单 5:配置通用资源适配器

清单 5 从第 2-35 行的资源适配器定义开始。存档在第 4 行定义——注意 EAR 文件名和 RAR 文件名之间的散列符号。请注意,您可能还需要在 RAR 文件名中添加 Maven 版本号。这取决于 EAR 中的物理文件,JBoss 以外的应用程序服务器可能使用不同的约定。第 6 行告诉应用程序服务器使用来自适配器的 XAResource ,以便它绑定到 XA 事务中。然后需要为要集成的每个 Web 服务重复第 8-32 行。第 9 行和第 10 行定义资源适配器提供的工厂,该值应始终为 ch.maxant.generic_jca_adapter.ManagedTransactionAssistanceFactory 。第 11 行定义了用于在 EJB 中查找资源的 JNDI 名称。第 12 行命名用于连接定义的池。建议为每个连接定义使用唯一的名称。第 13-15 行定义连接定义的 ID。每个连接定义必须使用唯一的名称。第 16-18 行告诉资源适配器在内部跟踪事务状态,以便它可以在没有正在集成的 Web 服务帮助的情况下处理恢复。默认值为 false,在这种情况下,您必须在清单 4 中注册一个恢复回调——参见下面的清单 6。如果资源适配器配置为在内部处理恢复,则第 19-21 行是必需的 - 您必须提供目录的路径,它应该在其中写入需要跟踪的事务状态。建议使用运行应用程序服务器的本地机器上的目录,而不是网络上的目录。 JBoss 需要第 22-31 行,以便它确实使用 XAResource 并将调用绑定到全局事务中。其他应用服务器可能只需要第 6 行——部署到其他应用服务器还没有完全测试( 更多细节... )。

恢复

直到现在我还没有说太多关于恢复的事情。实际上,清单 5 中 XML 中的 handleRecoveryInternally 属性意味着应用程序开发人员实际上不需要考虑恢复。然而,如果我们回到清单 2,恢复显然是两阶段提交协议的一部分。 事实上,维基百科指出,“为了适应故障恢复(在大多数情况下是自动的),协议的参与者使用协议状态的日志记录。协议的恢复过程使用日志记录,通常生成速度较慢但在失败后仍然存在。”。 参与者是资源管理器,例如数据库或 Web 服务,或者如果您希望这样解释的话,也可能是资源适配器。老实说,我不完全理解为什么事务管理器不能这样做。使资源适配器更灵活,而且如果不允许适配器写入文件系统(大公司的运营管理部门往往这样严格),也可以提供资源适配器带有回调,以便它可以向 Web 服务询问一组事务编号,Web 服务认为这些事务编号处于不完整状态。请注意,如果适配器的配置如上,那么它会跟踪对 Web 服务本身的调用状态。收到成功响应后,将调用 Web 服务的提交或回滚方法的信息保存到磁盘。如果应用程序服务器在写入信息之前崩溃了,那也不是那么悲惨,因为适配器会告诉事务管理器事务未完成,事务管理器将再次尝试使用 Web 服务提交/回滚。由于上面定义的 Web 服务契约要求可以多次调用提交和回滚方法而不会引起问题,因此当事务管理器随后尝试重新提交或重新回滚事务时应该绝对没有问题。这使我声明您想要注册恢复回调的唯一原因是您不允许让资源适配器写入磁盘。但我应该声明,我不完全理解为什么 XA 要求资源管理器提供可能不完整事务的列表,而事务管理器肯定能够自己跟踪此状态。

设置恢复以便适配器使用 Web 服务查询它认为不完整的事务,包括首先将部署描述符中的 handleRecoveryInternally 属性设置为 false (之后您不需要提供 recoveryStatePersistenceDirectory 属性),然后,添加一个恢复回调,如清单 6 所示。


 mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person; +-----+-------------------------------+ | id | name | +-----+-------------------------------+ | 771 | ant | +-----+-------------------------------+

清单 6:定义恢复回调

注册恢复回调是在注册清单 4 中设置的提交和回滚回调之后完成的。清单 6 的第 18-26 行添加了一个恢复回调。在这里,我们简单地创建一个 Web 服务客户端并调用 Web 服务来获取应该完成的交易 ID 列表。任何错误都被简单地记录下来,因为事务管理器很快就会过来再次询问。如果这里有错误,一个更健壮的解决方案可能会选择通知管理员,因为首先这里不应该发生错误,其次事务管理器从后台任务调用此回调,其中永远不会向用户显示错误。当然,如果 Web 服务当前不可用,事务管理器将不会收到任何事务 ID,但希望下次尝试时(在 JBoss Wildfly 8 中大约每两分钟一次),Web 服务将再次可用。

测试

为了测试适配器,我们构建了一个基于本文开头描述的场景的演示应用程序(也可在 Github 上获得),它调用三个 Web 服务并两次写入数据库,所有这些都是在同一个事务中完成的。获取者支持执行、提交、回滚和恢复;预订系统支持执行、提交和回滚;写信器只支持执行和回滚。在测试中,流程首先写入数据库,然后调用收单行,然后是预订系统,然后是写信人,最后更新数据库。这样,可以测试流程中多个点的故障。该适配器使用以下测试用例进行了测试:

  • Positive Testcase - 此处允许一切通过。 Afterwards the logs, database and web services are checked to ensure that indeed everything is committed.
  • Failure at end of process due to database foreign key constraint violation - Here the web services have all executed their business logic and the test ensures that after the database failure, the transaction manager rolls back the web service calls.
  • Failure during execution of acquirer web service - here, the failure occurs after an initial database insert to check that the insert is rolled back
  • Failure during execution of booking web service - here, the failure occurs after an initial database insert and the web service call to the acquirer to check that both are rolled back
  • Failure during execution of letter writer web service - here, the failure occurs after an initial database insert and two web service calls to check that all three are rolled back
  • During commit, web services are shut down - by setting breakpoints in the commit callbacks, we can undeploy the web services and then let the process continue. Initial committing on the web services fails but the database is fine and the data is available. But after the web services are redeployed and up and running, the transaction manager again attempts to carry out the commit which should be successful.
  • During commit, the database is shut down - also using breakpoints, the database is shutdown just before the commit. Commit works on the web services but fails on the database. Upon restarting the database, the next time the transaction manager runs a recovery it should commit the database.
  • Kill application server during prepare, before commit - Here we check that nothing is ever commit.
  • Kill application server during commit - Here we check that after the server restarted and recovery runs, that consistency across all systems is restored, ie that everything is committed.

结果

The demo application and resource adapter log everything they do, so the first port of call is to read the logs during each test. Additionally, the database writes to disk, so we can use a database client to query the database state for example select * from person p inner join address a on a.person_FK = p.id; . The acquirer writes to the folder ~/temp/xa-transactions-state-acquirer/ . There, a file named exec*txt exists if the transaction is incomplete, otherwise it is named commit*txt or rollback*txt if it was commit or rolledback, respectively. The booking system writes to the folder <jboss-install>/standalone/data/bookingsystem-tx-object-store/ . The letter writer writes to the folder <jboss-install>/standalone/data/letterwriter-tx-object-store/ . The adapter removes the temporary file named exec*txt once the transaction is commit or rolled back, so the only way to verify completion is to read the adapter logs, although checking that the files are removed makes sense, albeit doesn't inform whether there was a commit or a rollback.

The results were all positive and as expected although an ancient bug in Mysql provided a nice little challenge to overcome, which I will write about in a different article. If you have difficulty with your database, take a look at the JBoss manual which provides tips on getting XA recovery working with different databases.

常问问题

  • The service I am integrating only offers an operation to execute and an operation to cancel. There is no commit operation. No worries - this is acceptable and discussed above, where the contract that web services should fulfil is discussed. Basically, call the execute operation during normal business processing and the cancel operation only if there is a rollback. During the commit stage, don't do anything, since data was already committed during the call to the execute operation.
  • What happens if a web service takes a long time to come back online, after a business operation is executed but before the commit/rollback operation has been called? Transactions that require recovery may end up in trouble if they take a long time to come back online, because it is recommended that the systems behind the web service implement a timeout after which they clean up reserved but not booked (committed) resources. Take the example where a seat is reserved in a theatre during the execution but the final booking of the seat is delayed due to a system failure. It is entirely possible that the seat will be released after say half an hour so that it can be sold to other potential customers. If the seat is released and some time later the application server which reserved it attempts to book the seat, there could be an inconsistency in the system as a whole, as the other participants in the global transaction could be committed, indicating that the seat was sold, and for example money was taken for the seat, yet the seat has been sold to another customer. This case can occur in normal two phase commit processes. Imagine a database transaction that creates a foreign key reference to a record, but that record is deleted in a different transaction. Normally the solution is to lock resources, which the reservation of the seat is actually doing. But indefinate locking of resources can cause problems like deadlocks. This problem is not unique to the solution presented here.
  • Why don't you recommend WS-AT? Mostly because the world is full of services which don't offer WS-AT support. And the adapter I have written here is generic enough that you could be integrating non-web service resources. But also because of the locking and temporal issues which can occur, related to keeping the transaction open between the execution and commit stages.
  • Why not just create an implementation of XAResource and enlist it into the transaction using the enlistResource method? Because doing so doesn't handle recovery. The generic connecter presented here also handles recovery when either the resource or the application server crash during commit/rollback.
  • This is crazy - I don't want to implement commit and rollback operations on my web services! WS-AT is for you! Or an inconsistent landscape...
  • I'm in a microservice landscape - can you help me? 是的! Rather than letting your client call multiple microservices and then having to worry about global data consistency itself, say in the case where one service call fails, make the client call an "application layer", ie a service which is running in a Java EE application sever. That service should make calls to the back end by using the generic connector, and that way the complex logic required to guarantee global data consistency is handled by the transaction manager, rather than code which you would have to otherwise write.
  • The system I am integrating requires me to call its commit and rollback methods with more than just the transaction ID. You need to persist the contextual data that you use during the execution stage and use the transaction ID as the key which you can then use to lookup that data during commit, rollback or recovery. Persist the data using an inner transaction ( @RequiresNew ) so that the data is definitely persisted before commit/rollback/recovery commences - this way it is failure resistant.
  • The system I am integrating dictates a session ID and does not take a transaction ID. See the previous answer - map the transaction ID to the session ID of the system you are integrating. Ensure that you do it in a peristent manner so that your application can survive crashes.
  • The payment system I am integrating executes the payment on their own website, but the "commit" occurs over an HTTP call. Can I integrate this? 是的! Redirect to their site to do the payment; when they callback to your site, run your business logic in a transaction and using the transaction assistant execute a no-op method in the execution stage which will cause the commit callback to be called at commit time; in the commit callback make the HTTP call to the payment system to confirm the payment.

Using the Generic Adapter in Your Project

To use the adapter in your application you need to do the follwing things:

  • Create a dependency on the ch.maxant:genericconnector-api Maven module,
  • Write code as shown in listing 3 to execute business operations on the web services that your application integrates,
  • Setup commit and rollback callbacks as shown in listing 4, and optionally a recovery callback as shown in listing 6,
  • Configure the resource adapter as shown in listing 5
  • Deploy the resource adapter in an EAR by adding a dependency to the Maven module ch.maxant:genericconnector-rar and referencing it as a connector module in the application.xml deployment descriptor.

For more information, see the demo application .

Conclusions

The idea that I had, namely to bind web service calls into JTA transactions using a generic JCA resource adapter does work. It eliminates the need to build your own transaction management logic and it does ensure that there is consistency across the entire landscape, regardless of whether a transaction is committed or rolled back in the application code running in the Java EE application server.

Further Reading

A plain english introduction to CAP Theorem
Eventual consistency and the trade-offs required by distributed development
The hidden costs of microservices
Microservice Trade-Offs
Starbucks Does Not Use Two-Phase Commit

With thanks to Claude Gex for his review.

相关文章