vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-microservices-with-jakarta-ee》ECLIPSE MicroProfile and Transaction-Narayana LRA

Eclipse MicroProfile and Transactions - Narayana LRA

在前面的章节中 我讨论了如何使用 Jakarta EE 和带有 Thorntail 的 MicroProfile 规范来实现微服务架构。

我们使用 JPA 或 Data Access Object (DAO) 模式描述了用于与不同数据库类型交互的技术。您学习了如何使用 Java Transaction API (JTA) 提交操作。您了解了如何使用 JAX-RS 规范公开您的 API,以使用 JSON 实现 RESTful Web 服务来发送和接收数据。最后,我们讨论了如何遵循 TDD 方法并使用 Arquillian 测试我们的 API。

现在,是时候处理您需要在 MSA 体系结构中遵循的一种革命性方法——事务了。事务是企业应用程序的关键要素之一——在像微服务架构这样的分布式异构架构中保持数据完整性和一致性是主要挑战之一。

在本章中,我们将介绍以下主题:

  • Transactions
  • Transactions in microservices architectures
  • The saga pattern
  • Saga pattern implementation
  • MicroProfile Long Running Actions (LRAs)

Transactions

事务处理是企业应用程序中最重要的元素之一。

事务是为在其范围内运行的操作提供all or nothing 属性的工作单元。此外,它保证共享资源(例如,消息传递代理中的数据库记录或消息)受到保护,不会被试图同时更改它们的多个用户访问。事务处于一组不可分割的操作序列中——为了保证数据的完整性,事务中包含的所有操作都必须随着数据状态的成功更改而终止(在这种情况下,事务被定义为已提交)。否则,它们都不能生效(在这种情况下,事务被回滚或中止)。

您可以将事务视为一种实现数据正确性的机械辅助工具。

使用 Java EE/Jakarta EE 规范的开发人员可以通过两种方式管理事务,如下所示:

  • Container-managed transactions: This is the easiest way to handle transactions. Using Enterprise Java Bean (EJB) or a simple Java class with the @Transactional annotation, developers can delegate the life cycle of the transaction to a container that hides all of the complex processing necessary to handle the transaction. In this case, it's important to know and follow the rules and specifications of the platform and the container to know the exact behavior of your workflow, avoiding unexpected results.
  • Bean-managed transactions: This is a fine-grained way to handle transactions. Developers are responsible for starting and ending the transaction, and deciding its final state (committed, aborted, or rolled back). The container will provide a transaction manager implementation, but it will not handle anything on behalf of the developer, who will have total control of the workflow, but no protection from bugs or low-quality code.

ACID properties

事务必须具有四个基本特征,用首字母缩略词 ACID(原子性、一致性、隔离性、持久性)概括,才能正确实现其功能。其主要目标是在业务域状态变更操作过程中保持数据的正确性和一致性。

特点如下:

  • Atomicity: This property means that all operations that are included in a transaction must be executed as if they were a single block. With this behavior, the application has the ability to abort a transaction if an exception occurs, and to have all writes (performed by insert, update, and delete operations) discarded, preserving the original integrity state of the data. Otherwise, when the transaction commits successfully, all operations of a transaction must execute a valid commit of the operation (be it database writing, message consuming, and so on). Conversely, when the transaction fails, the system must be able to roll back the original status of the data, and all realized operations and effects must be undone.
  • Consistency: This property is related to the logical consistency of the data. When a new transaction starts, it must ensure that the data maintains a state of logical consistency, regardless of the final outcome (commit, abort, or roll back). In reality, the concept of logical consistency cannot be translated into a universal, semantic algorithm, applicable by a transaction manager; it is a state that is linked to the business domain implemented by the application. We can define it as a human concept; therefore, there is no semantic knowledge to be applied by the container. The only way to implement this concept is to restore the initial state of the data (in case of a failure), or to ensure that all operations are completed, bringing the data to a new state, if successful.
  • Isolation: This property is related to the execution of parallel transactions on the same resources. It guarantees that concurrent transactions cannot interfere with one another, like what happens for the thread-safe code. Each transaction behaves as if it was executed entirely alone. The serial execution of each transaction guarantees a consistent state, while the execution of parallel transactions cannot result in an inconsistent state of the data. The relational databases that follow the standards described in ANSI SQL-92 implement four levels of isolation with a different implementation of the locking mechanism. The four levels are as follows:
    • Read uncommitted: This is the lowest isolation level. Setting this property, the transaction only sets the lock when it needs to update the record item and releases it after the transaction is terminated. It's usually defined as long write locks. This behavior allows one transaction to read the changes of any other transactions that are not committed (a dirty read). However, it avoids the dirty write problem, which happens when a transaction overwrites a value that was previously written by another transaction and has not been committed yet.
    • Read committed: Using this property, the application is able to avoid the dirty reads phenomenon. This isolation level allows the read of data only when it has been committed. It is the main model that's used in enterprise applications.
    • Repeatable read: This isolation level prevents the phenomenon called non-repeatable read that can happen with the read committed setting. It involves reading the data values over different periods, during which a consistent data state may not be guaranteed. This means that, for example, a transaction could know of a data change at a later point in time: this could invalidate the data that was read during previous processing.
    • Serializable: This property specifies that one transaction is unable to read the data that has been modified by another transaction, but has not yet been committed. If you want to obtain the same result without setting this value you should implement a model that guarantees that no other transaction can insert new data that has a primary key with values that could fall within a key range read from any statement. So you should choose if delegate this behavior to the database or to implement a custom solution yourself.
  • Durability: This feature requires that all of the changes be made during a transaction; once it is committed, it must be persistent and definitive, even in the case of system crashes. Usually, the use of persistent storage, like a disk drive or the cloud, is sufficient.

除了每个solution 用来实现ACID属性的具体元素之外,你可能已经注意到,共同点是一个锁,这不是让我们获得一个架构中的高水平可扩展性。

谁是处理交易的参与者?答案是——事务管理器。

Transaction managers

事务管理器 负责协调跨一种或多种资源的事务。这保证了每个事务的完成,并决定是 commit 还是 rollback。其主要职责如下:

  • Starting and ending (with commit or abort state) transactions, using the begin, commit, and rollback methods
  • Coordinating the transactions across multiple resources
  • Managing the transaction context—the transaction manager creates it and attaches it to the current thread
  • Recovery from failure—this concept is implemented by using rollback capabilities that ensure the recovery of the original data state caused by a failure

有两种类型的事务管理器,本地和全局:

  • The local transaction manager is responsible for coordinating the transactions over a single resource; it's not able to provide ACID features across multiple resources (for example, database updates and Java Message Service (JMS) message sending).
  • The global transaction manager is used to coordinate transactions over multiple resources. Usually, it's provided by an external system (for example an application server) and it's able to guarantee ACID transactions between two or more different transactional resources.

事务管理器通过两个规范在 Java EE/Jakarta EE 中定义:

  • Java Transaction API (JTA), currently in version 1.2
  • Java Transaction Service (JTS)

JTA

JTA 是建立事务管理器 API 的规范,以及它应该与分布式事务系统中涉及的其他组件交互的方式:

  • The application
  • The resource manager
  • The application server

正如我之前解释的,您可以以容器管理的方式使用这些 API,也称为 声明性模型,或者以 bean 管理的方式,也称为 编程模型。管理事务的推荐方法是通过处理事务生命周期的所有阶段的容器。根据我的经验,只有在非常特殊的用例中才需要以编程方式管理事务,并且需要对事务、并发和恢复策略有深入的了解。

在第一种情况下(声明式模型),开发人员通常要求容器通过使用 EJB 来管理事务。

许多人认为 EJB 是一种复杂且不可扩展的技术。就我个人而言,我不同意这种评估,它是基于与该技术的第 2 版相关的负面偏见。从 3.0 版本开始,EJB 的使用大大简化;它们的配置,以及它们的使用,与 Spring 框架中的 bean 并没有太大的不同。

然而,对于那些不想被绑定到 EJB 上下文的人,在 JTA 的 1.2 版中引入了两个新的注释,@Transactional@TransactionScoped。这些注释使简单的托管 bean 能够像 EJB 一样工作,但对 EJB 容器没有任何依赖关系。它们还提供使开发人员能够为 bean 实例指定标准 CDI 范围的功能,其生命周期与当前活动的 JTA 事务相关。我们在 Chapter 4 中实现的微服务示例中使用了它们,使用 Thorntail 构建微服务

当事务管理器工作在 JTA 模式时,数据在内存中共享,事务上下文通过远程 EJB 调用传输。

无论使用何种技术,开发人员都需要配置容器,设置应如何管理单个事务。允许的值如下:

  • MANDATORY: The execution method of a client must happen inside of a transaction context. If there is a transaction context and the EJB method is executed inside its scope, the container uses it; otherwise, an exception is thrown.
  • REQUIRED: The execution method of a client must happen inside of a transaction context. The specification defines that if the transaction context is already present, it must be used; otherwise, a new transaction context will be started by the container. This is the default value for all methods defined in EJB.
  • REQUIRES_NEW: The container creates a new transaction context on every method's invocation. If a transaction context is already present, it will be suspended by the container for the duration of the execution of the new transaction.
  • NOT_SUPPORTED: The execution of a method happens only outside of a transaction context. If a transaction's context is created before the invocation of the method, it will be suspended and then the method will be invoked. Otherwise, the method will be executed without the creation of a new transaction context.
  • SUPPORTS: If the execution of a method happens with a transaction context, the container will behave in the same way as the REQUIRED attribute. Otherwise, the behavior of the container will be the same as what we described for the NOT_SUPPORTED attribute.

第二种情况(程序化模型),开发者使用javax.transaction.UserTransaction接口来处理整个事务生命周期,主要使用以下方法:

  • begin(): Start a new transaction, and associate it with the current thread
  • commit(): Complete the transaction associated with the current thread
  • rollback(): Rollback the transaction associated with the current thread
  • setRollbackOnly(): Update the transaction status associated with the current thread so that the only possible result of the transaction is to rollback

JTS

JTS 指定了支持以下内容的事务管理器的实现:

  • JTA specifications, at a high level
  • Java mapping of the Object Management Group (OMG), and Object Transaction Service (OTS) specification at a low level

事务管理器工作在JTS模式时,通过发送Common Object Request Broker Architecture(CORBA)消息共享数据,通过Internet传输事务上下文ORB 间协议 (IIOP) 调用。

由于与外部第三方 ORB 问题的互操作性,此方法的可移植性较差。使用此技术时,不同的应用程序服务器实现可能无法在它们之间分配事务。

Extended architecture

在前面的部分中,我们讨论了数据的状态如何在操作结束时永久保存。

但是,企业应用程序由复杂的操作组成,这些操作通常涉及多个异构数据源,例如数据库、消息代理、缓存等。

必须实现应用程序功能以确保数据的逻辑一致性;这意味着组成它的各个操作,虽然坚持使用不同的数据源,但仍必须作为一个单元。他们必须成功地完成所有事情,否则他们都不能影响数据的状态。

我们如何才能实现这种行为?

答案是Extended Architecture (XA) 规范。它是 X/Open 通用应用程序环境 (CAE) 标准规范,旨在定义使用多个后端数据存储的事务。其目标是定义规范以使事务管理器将与单个数据源(应用程序服务器、数据库、缓存和消息队列)关联的所有事务作为单个全局 XA 事务进行协调。

在 XA 架构中,涉及两个主要参与者,如下所示:

  • The global transaction manager, who is responsible for coordinating the different transactions as a single unit
  • The XA resource, which represents the data source target of the single transaction

在 Java EE/Jakarta EE 中,与跨多个资源处理分布式事务相关的 XA 标准由 javax.transaction.xa.XAResource 接口实现。它定义了资源管理器和事务管理器应如何在分布式事务处理 (DTP) 环境中进行通信。

分布式事务,也称为 XA 事务,是一种 ACID 事务,由独立的参与者执行,这些参与者通过通信网络相互连接。

在全局分布式事务中,每个节点都有自己的事务管理器来管理本地事务——完成后,事务管理器与其他参与者的事务管理器进行通信,共享事务的状态以决定最终结果。

通常,在扩展架构中,一个事务管理器被选为全局事务协调器——它管理分布式事务中的其他参与者,它可以是参与节点之一,也可以是一个独立的服务。

全局事务管理器使用什么规则来决定 XA 事务的结果?

决策策略被称为共识协议。

Consensus protocol

共识协议是分布式系统的不同组件就共享数据价值达成一致的方式。事务管理器使用事务环境来确定事务是否可以提交或中止,使用以下三个条件:

  • Agreement: All nodes decide on the same value
  • Validity: If all of the nodes have the same value, then a consensus must be obtained on this value
  • Termination: All nodes decide about a transaction's outcome

在企业架构中,主要有以下三种共识协议:

  • Two-phase commit (2PC) protocol
  • Three-phase commit (3PC) protocol
  • Paxos

2PC protocol

2PC是最常用的共识算法。在这种情况下,全局事务管理器联系每个参与者,建议一个可能的值,收集每个参与者的响应,如果每个人都同意,则决定 commit;否则,它会就共识与abort 进行通信,并对事务执行rollback

该过程包括两个阶段,如下所示:

  • The prepare phase: All participants send their proposals to the coordinator, stating whether they are able to proceed and commit their single operation, or whether the transaction should be aborted. This phase is implemented by the prepare() method of the javax.transaction.xa.XAResource interface.
  • Commit or abort phase: All participants communicate the result of the vote, and tell the coordinator to either go ahead and decide to commit, or to abort the transaction. The coordinator specifies the behavior of the resources. The resources notify the coordinator when they finished to do this and when the transaction has finished. This is implemented by the commit and rollback methods of the javax.transaction.xa.XAResource interface.

2PC 共识协议具有通过事务日志管理节点故障的能力。通常,定期恢复线程会处理所有未完成的事务。

2PC 协议的主要缺点之一是它是基于锁的协议。如果协调器在第一阶段完成后失败,所有参与者将被阻塞等待协调器的决定,直到它恢复才能继续。

3PC protocol

3PC 协议是 2PC 协议的扩展,它的目标是克服它的限制,这与它的阻塞特性有关。

该共识协议的主要特点之一是它是非阻塞的;这并不意味着参与者在处理过程中没有被阻止,而是意味着协议可以在失败的情况下继续进行。

它由三个阶段组成; 准备阶段和提交(或中止)阶段与2PC协议的相同。

然而,它引入了一个新阶段,我们可以将其定义为准备状态,交易的所有参与者都将定义他们的状态。状态可以是等待或预提交。这意味着最后一个阶段只能有一个最终状态——如果前一个阶段正在等待,,则为中止;如果前一个阶段是预提交,则提交,。

3PC 减轻了 2PC 的阻塞特性。问题是它通过更复杂的协议获得它,并且需要在决定提交或中止事务之前发送另一条消息。此外,它不能解决网络分区的问题。

3PC在生产环境中应用并不广泛。

The Paxos consensus

Paxos 共识协议由 Leslie Lamport 于 1998 年引入,目标是克服 2PC 和 3PC 协议的限制。 当系统中的大多数参与者都在场并且消息延迟为 < span class="TextRun Highlight SCXW148592624">minimal,在这种情况下系统没有被屏蔽。

它由三个主要参与者组成,如下所示:

  • Proposer: This component has the responsibility to initiate the protocol. It proposes, as name says, the possible values of which the client wants the entire system to agree on. It send a proposal number to the acceptors and it waits for a response from a majority of them. In case most of the acceptors agree, the value of any previously accepted proposal will be returned; if the majority reply with reject, or fail to reply, they abandon the proposal and start again.
  • Acceptor: This component responds to the proposal performed by the proposer. All of the nodes that participate in the transaction are required to know the quorum of acceptors that form a majority. Once the acceptor receives a proposal, it compares its value to the highest value proposal that the client has already accepted. If the new proposal is higher, it replies agree; otherwise, it replies reject.
  • Learner: This component is responsible to learn which value was chosen by the acceptors in the consensus phase.

该协议基于两个阶段,如下所示:

  • Promise phase: In this phase, the proposer proposes a value on which to establish a consensus. Then, it communicates this value to all of the acceptors (or to the quorum) using a unique identification number generated from a sequence. The acceptor receives the proposed value and checks if it is higher than the last ID that the client has already agreed to. If this evaluation returns true, the acceptors will respond to the proposer with a promise message; otherwise, no action will be taken.
  • Commit phase: In this phase, the proposer collects all the answers it received by the acceptors. If the majority answered with a promise, the proposer sends them an accept-request. The majority of acceptors reply with an accept message to the proposer and learners that finally accept it. In this way, all the members involved in a distributed environment reach the consensus and decide to commit the transaction. Otherwise, a rollback operation will be performed.

Paxos 是第一个在异步网络中具有弹性的协议。当网络行为异步而不是正确性时,它更愿意牺牲活性。它仅在同步返回时终止操作。

尽管如此,它还是有一些问题;两个提议者试图获得最高提议号的并发竞争会导致系统阻塞,直到冲突解决。

共识是一个非常复杂和复杂的问题;在编写本书时,在 Java EE/Jakarta EE 环境中,最常用的协议是 2PC。

Transactions in microservices architecture

与微服务架构相关的好处是有目共睹的,从缩短上市时间,到更简单的源生命周期管理,再到根据实际工作量对资源进行准时管理。

然而,在每次关于微服务的讨论结束时,在进行应用架构设计之前,总会出现一个问题——微服务中的事务是如何管理的?

由于许多原因,分布式系统交互可能很复杂,其中一些原因如下:

  • A microservices architecture involves many parties, realized using different technologies that adhere to different specifications
  • A business function can span many different organizations that can have different service-level agreements, and, for this reason, can be implemented with various strategies
  • A business function potentially lasting for hours or days, a logic that hardly reconciles with scalability—indeed it's very difficult to scale an application that has a logic based on the lock, performed by a transaction, which makes the system irresponsive for a long time

为了保持其高可扩展性特性,微服务可能无法无限期地锁定资源。

一个业务功能可以由许多微服务组成,每个微服务负责管理一个特定的数据域。在要实现的业务功能需要实现更多步骤的复杂情况下,可能只需要撤消之前完成的工作子集。

然而,前几节中表达的概念(与事务的 ACID 属性有关)在以前的架构模型(包括 SOA 的架构模型)中始终可以正常工作。那么,为什么要放弃这些?而且,如果我们被迫这样做,如何管理数据的完整性?

在微服务架构中使用 JTA(尤其是 XA)并不容易。原因在于 ACID 事务的性质,其中隐含假设如下:

  • Have a closely coupled environments where all entities involved in a transaction span a LAN
  • Have activities with a short execution time that must be able to work properly when the resources are locked for periods of time

微服务基于截然相反的概念,例如松散耦合的环境和长期活动。此外,微服务是分布式系统,通过网络进行通信会带来一些复杂性,例如由于网络开销导致的网络故障和通信性能问题。失败和性能是交易发生率较高的两个方面。

1997 年,Java 之父 James Gosling 扩展了 Peter Deutsch 创建的草案,该草案陈述了通常对分布式系统做出的错误假设。

这些假设被称为分布式计算的八个谬误,它们如下:

  • The network is reliable
  • The latency is zero
  • The bandwidth is infinite
  • The network is secure
  • The topology doesn't change
  • There is one administrator
  • The transport cost is zero
  • The network is homogeneous

正如在共识协议部分已经讨论过的,所有这些因素都对交易管理产生了重要影响。

微服务架构用一个词解决了这一系列问题:放松。

它提出了一种关于 ACID 属性的不太严格的方法,如下所示:

  • Atomicity: The microservices architecture approach prefers to undo a portion of work rather than cancel all of the work. For example, imagine you're booking a holiday—I'm sure that you prefer to buy an airline ticket, maybe when it has a very good price, even without travel insurance, rather than failing to book the trip and, consequently, stay at home only because the insurance service is temporarily unavailable. This model is similar to that of nested transactions where the work performed within their scope is provisional. A nested transaction represents a problem from a JTA point of view; support for nested transactions is not required, as declared in the specifications, and XAResource does not support nested transactions. So, it's very difficult to implement this concept in the Java EE/Jakarta EE environment. In the end, the mantra must be that failure does not affect enclosing the transaction.
  • Consistency: ACID transactions, and the two phase commit protocol, are based on strong global consistency. All participants stay in the lock step and they must retrieve the same transaction outcome. But this approach doesn't scale, and weak consistency replication protocols were developed for large scales. A weak or relaxed consistency is the approach proposed by microservice architectures. This model is known as eventual consistency, also known as optimistic replication—its main concept is that the system guarantees that, if no new updates are performed to the requested object, all of the data reads will return the last updated value. With this approach, the system does not provide any guarantees about the consistency of data for a limited period, also defined as inconsistency window. Microservices must manage the eventual consistency model to avoid to make decisions based on inconsistent information. Distributed systems, such as microservices, are unable to guarantee both strong consistency and high availability at the same time. For this reason,distributed business applications are often chosen to tolerate temporary data inconsistencies in favor of promoting availability. This approach is also classified as Basically Available, Soft state, Eventual consistency (BASE) in contrast to traditional ACID semantic.
  • Isolation: In this case, the microservice approach is to delegate the isolation of the resources to a service provider. It could decide to commit early and perform a compensation actions later. The important thing is that the undo operation, which is needed for the system to come back to its original state, must be always available.

The saga pattern

saga 模式是微服务架构提出的解决方案,用于管理分布式系统中的事务。

saga 是一系列操作,表示可以通过补偿操作撤消的工作单元。当操作成功时,它会发布消息或事件以触发 saga 中的下一个本地事务;否则,saga 会执行一系列补偿事务,以撤消前面所做的更改。每个操作都可以看作是一个本地事务;因此,它对自己的数据源执行 commitrollback,但与构建 saga 的所有其他操作或本地事务进行通信。

saga 保证所有操作成功完成,或者为所有执行的操作运行相应的补偿操作,以取消部分处理。

这种方法不同于 2PC 协议所遵循的方法,后者创建了一个全局分布式事务 (XA),涉及构建业务功能的所有资源/服务。 saga 模式实现了 divide et impera 的概念——每个服务都在本地事务中运行并提供补偿操作。构成业务功能的所有单个操作的集合通过使用事件(事件/编排模式)或 Saga 执行协调器(命令/编排)进行通信。

可以立即注意到,由 saga 模式实现的方法违反了 ACID 事务隔离的原则;提交部分操作的能力打破了隔离,因为它使段更改在 saga 结束之前可用。 Saga 通过使用最终一致性模型克服了这种方法,正如我之前所描述的,该模型保证状态最终将在 saga 完成后变得一致。

因此,saga 模式使用了 ACID 的替代方案;我们可以将其定义为 BASE。该首字母缩写词总结了以下属性:

  • Basically available: The system guarantees availability, as defined in the CAP theorem, published by Seth Gilbert and Nancy Lynch in 2002, stating that network shared data systems can only guarantee support for two of the following three properties:
    • Consistency: Every node in a distributed cluster returns the same, most recent, successful write, so that every client has the same view of the data
    • Availability: Every available node returns a response for all read and write requests in a reasonable amount of time
    • Partition tolerant: The system continues to function and maintains its consistency guarantees, in spite of network partitions
  • Soft state: The state may change as time progresses, even without any immediate modification requests, due to the eventual consistency.
  • Eventual consistency: As I described earlier, the state of the system is allowed to be in inconsistent states for short periods of time. If the system does not receive any new update requests, then it guarantees that the state will eventually get to a consistent state.

saga 模式的关键要素之一是补偿。它的作用是撤消由原始操作执行的工作,但使用另一个操作,而不是使用事务的回滚的常用方法。

如前所述,原因不仅与技术方面有关,还与不同的功能方法有关。补偿动作不一定要将数据的状态恢复到初始状态;它的功能是将数据的状态设置为与正在处理的业务域一致的值,以防阻止成功完成请求的原始操作的 saga 操作失败。

重要的是补偿必须是幂等的,因为只有这样才能拦截故障并实施强恢复管理。

主要原因是连赔偿都可能失败;与由数据库或应用程序服务器保证的传统事务 rollback 不同,它不保证始终工作。失败的原因是异构的,因此很难实现一种能够拦截所有的算法。

您可以实施两种不同的策略,如下所示:

  • Backward recovery: This is the most common approach, and it requires that all operations define a compensation handler. In the case of failure, the saga execution component aborts the currently executed operation, and then, for every previous successful operation, in the reverse order of the original execution, it calls its respective compensation action.
  • Forward recovery: This strategy requires that the system is able to produce a checkpoint that represents a snapshot of the system state at that particular point in time, to which the system can always be restored. This concept is similar to the one used in business process management applications. This effectively eliminates the need to define any compensation actions, since the system, in the case of a failure, will always have a safe point from which it can try to complete the business operation. Using this approach, the saga execution component is reduced to a basic, persistent transaction executor, losing most of the saga's benefits.

也可以将这两种方法结合起来以获得每种方法的好处。

交易系统以预定义的时间间隔制作检查点,该时间间隔可以是定期的或基于不同的标准。在失败的情况下,系统执行向后恢复到最后定义的检查点,然后在向前恢复模式下继续 saga 执行。

Saga implementations

正如我们在 Saga 模式 部分所述,saga 模式正在成为微服务中事务管理的事实标准。

有几个框架实现了 saga 模式。在接下来的部分中,我们将快速分析主要部分,重点关注 MicroProfile 社区中正在开发的实施建议。

The Axon framework

Axon 是一个轻量级框架,实现为 Java Spring Boot 微服务应用程序,内置于独立的 Maven 项目中,并以可执行 JAR 的形式分发;它基于Command Query Responsibility Segregation (CQRS) 原则。这里的主要概念是您可以为 update 操作(updateinsert)和 read 操作。

Axon 使用两种不同的渠道在服务之间交换信息:

  • A command bus
  • An event bus

命令总线是让目标接收命令对象的参与者,包含与要执行的操作相关的所有数据;它将它转发给命令处理程序。命令处理程序执行命令对象指定的操作(和业务方法)。

此操作的结果是生成形成领域模型的领域事件。

事件总线将事件同步或异步分派给事件侦听器,事件侦听器执行操作(例如,更新数据源或向外部系统发送消息)。

命令处理程序完全不知道对它们所做的更改感兴趣的组件;通过这种方式,您可以构建松散耦合的架构。

Axon 使用两种不同的命令总线实现——JGroups 连接器和 Spring Cloud 连接器,基于 Netflix Eureka Discovery 客户端和 Eureka Server 组合。

对于事件总线,框架使用RabbitMQ,一个支持多种消息协议的外部开源消息系统;在 Axon 实现中,事件总线建立在 高级消息队列协议 (AMQP) 之上。

Axon 是一个非常强大的框架,但是如果你想用它来处理你系统中的事务,你必须意识到以下挑战:

  • Maintenance of the saga life cycle: Axon provides only the implementation to start and stop the saga execution. The handling of some important aspects of the saga pattern is up to the developer—for example, the invocation of participants, the relative collection of responses to define the final state of the transaction or the handling of the compensations actions, in order to recover the original data state after a failure, are delegate to the developer since the framework implements the communication between the participants of the saga only through events. This implementation model of the saga specification could become a potential bottleneck of the saga pattern's maintenance.
  • CQRS restrictions: In Axon, the sagas are only a specialized type of event listener: this implementation model could be too restrictive in an environment that doesn't use the CQRS pattern.

Eventuate

Eventuate 是一个用于开发异步微服务的平台,与 Axon 框架一样,它使用了事件溯源模型和 CQRS 模式。这两种方法的主要区别在于,在 Eventuate 平台中,命令和事件总线是本地的,而不是分布式的。另一个区别是 Eventuate 只允许使用 REST 协议进行远程通信。

该平台由两个产品组成,如下所示:

  • Eventuate ES: This product uses an approach similar to the one of the Axon framework, which is based on an event sourcing programming and a persistence model.
  • Eventuate Tram: This is especially suitable for microservices that perform CRUD operations through the use of JPA/JDBC. It is easily integrated with the Spring framework, and it overcomes some limits of Eventuate ES, for saga handling in particular.

Eventuate ES

Eventuate ES 应用程序由以下类型的模块组成:

  • Command-side module: It creates, updates, and aggregates objects that implement business logic and are persisted using event sourcing, in response to external update requests (usually HTTP POST, HTTP PUT, and HTTP DELETE requests) using services objects, and events published by command-side aggregates.
  • Query-side module: It maintains the materialized CQRS views of command-side aggregates. This module has two sub-modules, as follows:
    • View update module: This subscribes to events to update the view
    • View query module: This handles query requests by querying the view
  • Outbound gateway module: This processes events by invoking external services.

Eventuate ES 在 saga 管理方面存在一些局限性,主要涉及平台的复杂性和 CQRS 模式的限制。

Eventuate Tram

Eventuate Tram 平台为 saga 管理实现了不同的解决方案,所有这些解决方案都是使用 Eventuate 平台构建的。主要思想是将消息作为数据库事务的一部分发送;通过这种方式,您可以原子地更新状态并发送消息或域事件,保持数据一致性。

Eventuate Tram 应用程序的架构,取自 https://bit.ly/2UtmFp4 上的示例a href="https://github.com/eventuate-tram/eventuate-tram-examples-java-spring-todo-list" target="_blank">

在观察到的图中,可以看到两个不同的服务:Todo ServiceTodo View Service 。 T他第一个实现了与域状态变化相关的功能(insert, update delete),并在其中之一之后发布将存储在 MESSAGE 表 以传统的 ACID 事务方式。

Eventuate Tram CDC 服务 跟踪插入到 MESSAGE ,通常使用数据库日志,作为一种变更数据捕获,并使用 Apache Kafka 将消息发布到分布式主题.

最后,Todo View Service订阅事件并更新ElasticSearch 以检索数据的最新版本。

如您所见,整体架构融合了 saga 编排和 CQRS 模式的优势。

MicroProfile LRA

MicroProfile 规范旨在基于 saga 模式提出一个标准解决方案,将分布式系统中的事务管理为微服务。

该规范是针对 MicroProfile 的 Long Running Actions (LRA):它由 Narayana 团队在 Eclipse MicroProfile 倡议下构建。目前,它处于“进行中”状态,是社区讨论的主要话题之一。

它提出了一种新的 API 来协调长期运行的活动:该方法将与在扩展架构中实现的方法完全不同。目标是保证全局一致的结果,而无需对限制系统可扩展性的数据设置锁定。

Narayana LRA 实现基于 Java EE/Jakarta EE 和 MicroProfile 提供的标准规范(特别是 上下文和依赖注入 (CDI) 和 JAX-RS)。

与我们之前描述的框架不同,Narayana(以及通常的 LRA 规范)使用编排传奇模型。

Narayana 框架中的主要参与者如下:

  • LRA coordinator: This manages the saga processing, and can be a standalone service or embedded within an application service. Its main responsibilities are the LRA's initialization, participant enlisting, and either saga completion or compensation.
  • Saga participant: This is a service that is involved in the LRA. Each one is required to provide at least one REST endpoint that serves as the compensation handler.

LRA 操作的执行是通过调用 LRA 协调服务来完成的。该服务以与发起服务相关的唯一标识值进行响应。

当业务服务由多个微服务构建时,需要对功能中涉及的所有参与者进行分组和协调。在 LRA 实施中,系统使用协调器返回的 ID 将参与者登记到 LRA 协调器。在过程结束时,无论是成功还是失败,知道 LRA 标识(ID)的参与服务之一联系协调器以关闭(成功)或补偿(失败)LRA。协调器为每个登记的参与者执行相应的请求动作。

saga 交易中涉及的参与者可以有两种状态,如下所示:

  • Success: The activity has completed successfully, so the participants can consider the transaction closed
  • Fail: The activity has completed unsuccessfully, and all participants involved in the LRA must perform compensation in the reverse order

让我们开始实现一个示例场景。

The football market microservice

在本节中,我们将实现一个 LRA 场景。

该用例与购买创建我们的幻想团队所需的球员有关。

我们将构建五个actor,如下所示:

  • The LRA coordinator
  • A microservice that handles the football player registry (football-player-microservice-lra)
  • A microservice that handles the purchase offers for the football players (football-player-offer-microservice-lra)
  • A microservice that simulates an API gateway that is responsible for implementing the business logic related to the football player market season (football-player-market-microservice-lra)
  • A simple client that performs the offers to buy the football players (Football_Player_Market_client)

LRA coordinator

为了管理 LRA,我们需要一个管理 saga 处理的协调器。我们选择了一个 Narayana 实现,作为一个独立的服务来实现这个概念。

您可以在 http://narayana.io/downloads/index.html 下载它;对于我们的实现,我们将使用 5.9.0 版本。

下载后,您需要将其解压缩到您选择的目录,从现在开始,该目录将被定义为 $NARAYANA_HOME

最后,您应该使用以下命令运行 Narayana LRA 协调器:

$ java -jar $NARAYANA_HOME/rts/lra/lra-coordinator-swarm.jar   -Dswarm.http.port=8580 

我们现在已将协调器作为独立服务启动,它侦听端口 8580 以避免与我们在 第 3 章云原生应用程序, 以及我们将在以下部分构建的新服务。

最后,在服务的日志中,您应该注意到以下消息,这些消息确认 Narayana 协调器已启动并正在运行:

2018-08-23   16:43:52,134 INFO  [io.narayana.lra] (ServerService Thread Pool -- 6)   LRAClient assuming the LRA coordinator and recovery coordinator are on the   same endpoing
2018-08-23 16:43:52,170 INFO [org.wildfly.extension.undertow] (ServerService Thread Pool -- 6) WFLYUT0021: Registered web context: /
2018-08-23 16:43:52,199 INFO [org.jboss.as.server] (main) WFLYSRV0010: Deployed "lra-coordinator.war" (runtime-name : "lra-coordinator.war")
2018-08-23 16:43:52,200 INFO [org.wildfly.swarm] (main) WFSWARM99999: WildFly Swarm is Ready

Football-player-microservice-lra

该服务是我们在第3章, < em>云原生应用程序

它的目标是处理足球运动员登记;在这个版本中,我们将使用 MicroProfile 规范来构建 LRA saga 规范。为了便于理解规范的使用,我们将只更改edit方法对事务的管理。

要使用 LRA 规范,我们将更改 Maven pom.xml;以下代码片段显示了我们所做的更改:

<?xml version="1.0"   encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
...

<properties>
<version.thorntail>2.1.0.Final</version.thorntail>
...
<lra.http.host>localhost</lra.http.host>
<lra.http.port>8580</lra.http.port>
</properties>

<build>
<finalName>football-player-microservice-lra</finalName>
<plugins>
<plugin>
<groupId>io.thorntail</groupId>
<artifactId>thorntail-maven-plugin</artifactId>
<version>${version.thorntail}</version>
<executions>
<execution>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
<configuration>
<properties> <lra.http.host>${lra.http.host}</lra.http.host> <lra.http.port>${lra.http.port}</lra.http.port>
</properties>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
...
<dependency>
<groupId>io.thorntail</groupId>
<artifactId>microprofile</artifactId>
</dependency>
...
<!-- LRA JAXRS filters -->
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-filters</artifactId>
<version>5.9.0.Final</version>
</dependency>
...
</dependencies>
</project>

主要变化如下:

  • The Thorntail version needed to use the MicroProfile specification—2.1.0
  • The host and port needed to connect with the LRA coordinator (in our case, localhost and 8580)
  • The new dependencies related to MicroProfile and lra-filters

然后,我们需要更新领域模型,FootballPlayer.java,引入两个新字段——足球运动员的statusfree,< kbd>reserved 或 purchased) 和 lraId 表示更新与关联的记录状态所需的长时间运行操作的唯一标识符正确的行动。

此外,在这种情况下,我们将提出所做更改的代码,如下所示:

public class FootballPlayer {   
    ...

@Column(name = "status")
private String status;

@Basic(optional = true)
@Size(min = 1, max = 100)
@Column(name = "lraId")
private String lraId;

...

public FootballPlayer(String name, String surname, int age, String team, String position,
BigInteger price, String status, String lraId) {
this.name = name;
this.surname = surname;
this.age = age;
this.team = team;
this.position = position;
this.price = price;
this.status = status;
this.lraId = lraId;
}

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}

public String getLraId() {
return lraId;
}

public void setLraId(String lraId) {
this.lraId = lraId;
}

...
}

最后,我们将更改 REST 端点(仅 edit/PUT 方法)以使用 LRA 模型处理 saga 实现。代码如下:

@ApplicationScoped
@Path("/footballplayer")
public class FootballPlayerFacadeREST extends AbstractFacade<FootballPlayer> {
...

@PUT
@Path("{id}")
@Consumes({MediaType.APPLICATION_JSON})
@Transactional(Transactional.TxType.REQUIRES_NEW)
@LRA(LRA.Type.SUPPORTS)
public void edit(@PathParam("id") Integer id, FootballPlayer entity) {
super.edit(entity);
}

...

@GET
@Path("lraId/{id}")
@Produces({MediaType.APPLICATION_JSON})
public FootballPlayer findByLraId(@PathParam("id") String lraId) {
TypedQuery<FootballPlayer> query = getEntityManager().createQuery(
"SELECT f FROM FootballPlayer f WHERE f.lraId = :lraId",
FootballPlayer.class);
return query.setParameter("lraId", lraId).getSingleResult();
}

...
}

我引入了带有 SUPPORTS 值的 @LRA 注释;这意味着,如果在 LRA 上下文之外调用,则 bean 方法的执行必须在 LRA 上下文之外继续。否则,它必须在该 LRA 上下文中继续。 LRA 上下文将由我们稍后构建的粗粒度 API 网关创建。

我还实现了 findByLraId 方法,该方法用于检索与 LRA 上下文关联的记录以执行操作。

现在,我们准备好使用以下命令运行微服务:

$ java -jar target/football-player-microservice-lra-thorntail.jar -Dlra.http.port=8580   

请记住设置 lra.http.port 参数以将您的服务与 LRA 协调器连接。

您可以在 GitHub 存储库中找到完整的代码实现,网址为 https://github。 com/Hands-on-MSA-JakartaEE/ch5.git

Football-player-offer-microservice-lra

这个微服务实现了购买足球运动员的提议的业务领域。

Maven pom.xml 文件与之前描述的文件类似。

我们将使用 MySQL 而不是 PostgreSQL 来为每个微服务强制执行专用数据库模式的概念。

以下是实体FootballPlayerOffer.java的属性代码,用于显示信息,由该微服务管理:

   @Entity
@Table(name = "FOOTBALL_PLAYER_OFFER")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "FootballPlayerOffer.findAll", query
= "SELECT f FROM FootballPlayerOffer f")
})
public class FootballPlayerOffer {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;

@Basic(optional = false)
@NotNull
@Column(name = "ID_FOOTBALL_PLAYER")
private Long idFootballPlayer;

@Basic(optional = false)
@NotNull
@Column(name = "ID_FOOTBALL_MANAGER")
private Long idFootballManager;

@Column(name = "price")
private BigInteger price;

@Basic(optional = true)
@Size(min = 1, max = 50)
@Column(name = "status")
private String status;

@Basic(optional = true)
@Size(min = 1, max = 100)
@Column(name = "lraId")
private String lraId;

public FootballPlayerOffer() {
}

public FootballPlayerOffer(Long id, Long idFootballPlayer,
Long idFootballManager, BigInteger price, String status, String lraId) {
this.id = id;
this.idFootballPlayer = idFootballPlayer;
this.idFootballManager = idFootballManager;
this.price = price;
this.status = status;
this.lraId = lraId;
}

...
}

就像我们对 football-player-microservice-lra 微服务所做的那样,我们只会在 edit/ 上关联 @LRA 注释PUT method 使用 LRA 模型处理 saga 实现。需要以下代码:

@ApplicationScoped
@Path("/footballplayeroffer")
public class FootballPlayerOfferFacadeREST extends AbstractFacade<FootballPlayerOffer> {
...

@PUT
@Path("{id}")
@Consumes({MediaType.APPLICATION_JSON})
@Transactional(Transactional.TxType.REQUIRES_NEW)
@LRA(value = LRA.Type.SUPPORTS)
public void edit(@PathParam("id") Long id, FootballPlayerOffer entity) {
super.edit(entity);
}

...

@GET
@Path("lraId/{id}")
@Produces({MediaType.APPLICATION_JSON})
public FootballPlayerOffer findByLraId(@PathParam("id") String lraId) {
TypedQuery<FootballPlayerOffer> query = getEntityManager().createQuery(
"SELECT f FROM FootballPlayerOffer f WHERE f.lraId = :lraId",
FootballPlayerOffer.class);
return query.setParameter("lraId", lraId).getSingleResult();
}

...
}

现在,您已准备好使用以下命令运行微服务; @LRA注解中使用SUPPORTS值的原因和findByLraId方法的引入是一样的正如使用 football-player-microservice-lra 时看到的那样:

$ java -jar   target/football-player-offer-microservice-lra-thorntail.jar -Dlra.http.host=localhost   -Dlra.http.port=8580 

此外,在这种情况下,您必须记住设置 lra.http.portlra.http.host 参数,以便您可以将服务与 LRA 协调器连接。

您可以在 GitHub 存储库中找到完整的代码实现,网址为 https://github。 com/Hands-on-MSA-JakartaEE/ch5.git

Football-player-market-microservice-lra

这是我们场景的关键模块。

在这里,我们将对微服务 football-player-microservice-lrafootball-player-offer-microservice-lra microservices 的调用组织为是实现我们的业务逻辑所必需的。

我们将详细分析这个微服务的两个关键点:

  • The Maven pom.xml, with the right dependencies
  • The RESTful endpoint to understand the logic of the LRA specifications

我们先从pom.xml开始,如下:

<?xml   version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.packtpub.thorntail</groupId>
<artifactId>football-player-market-microservice-lra</artifactId>
<name>Thorntail Football player market microservice LRA</name>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<version.thorntail>2.1.0.Final</version.thorntail>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<failOnMissingWebXml>false</failOnMissingWebXml>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.resteasy>3.0.19.Final</version.resteasy>
<lra.http.host>localhost</lra.http.host>
<lra.http.port>8580</lra.http.port>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.thorntail</groupId>
<artifactId>bom</artifactId>
<version>${version.thorntail}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<finalName>football-player-market-microservice-lra</finalName>
<plugins>
<plugin>
<groupId>io.thorntail</groupId>
<artifactId>thorntail-maven-plugin</artifactId>
<version>${version.thorntail}</version>

<executions>
<execution>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<lra.http.host>${lra.http.host}</lra.http.host>
<lra.http.port>${lra.http.port}</lra.http.port>
<swarm.port.offset>400</swarm.port.offset>
</properties>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>io.thorntail</groupId>
<artifactId>microprofile</artifactId>
</dependency>
<dependency>
<groupId>io.thorntail</groupId>
<artifactId>transactions</artifactId>
<scope>provided</scope>
</dependency>
<!-- LRA JAXRS filters -->
<dependency>
<groupId>org.jboss.narayana.rts</groupId>
<artifactId>lra-filters</artifactId>
<version>5.9.0.Final</version>
</dependency>
<dependency>
<groupId>io.thorntail</groupId>
<artifactId>jaxrs</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.thorntail</groupId>
<artifactId>cdi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>${version.resteasy}</version>
<scope>provided</scope>
</dependency>
<!-- CORS Support For JAX-RS 2.0 / JavaEE 7 -->
<dependency>
<groupId>com.airhacks</groupId>
<artifactId>jaxrs-cors</artifactId>
<version>0.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

要点如下:

  • The minimum Thorntail version that's required is 2.1.0
  • lra.http.port and lra.http.host are needed to connect to the LRA coordinator
  • The dependencies are needed to perform RESTful invocations and to handle the LRA

现在,我们将分析 RESTful 端点。

我们将分类一个由四部分组成的类来分析实现的关键要素,如下所示:

  • Initialize
  • A business method that represents our LRA
  • The complete phase
  • The compensate phase

Initialize

在此阶段,该类初始化执行对其他微服务的调用所需的所有元素,并通过 LRA 实现获得与 LRA 协调器交互所需的 LRAClient


@ApplicationScoped
@Path("/footballplayer-market")
public class FootballPlayerMarketREST {

private static final Logger LOG =
Logger.getLogger(FootballPlayerMarketREST.class.getName());

private Client footballPlayerClient;

private Client footballPlayerOfferClient;

private WebTarget footballPlayerTarget;

private WebTarget footballPlayerOfferTarget;

@Inject
private LRAClient lraClient;

@PostConstruct
private void init() {
footballPlayerClient = ClientBuilder.newClient();
footballPlayerOfferClient = ClientBuilder.newClient();
footballPlayerTarget = footballPlayerClient.target(
"http://localhost:8080/footballplayer");
footballPlayerOfferTarget = footballPlayerOfferClient.target(
"http://localhost:8680/footballplayeroffer");
}

@PreDestroy
private void destroy() {
footballPlayerClient.close();
footballPlayerOfferClient.close();
}
...
}

需要注意的最重要的一点是,LRAClient 会自动注入到容器中,并使用lra.http.portlra。启动时给出的 http.host 属性将自动连接到 LRA 协调器。 显然,与要调用的微服务相关的 URL 应该被参数化,以便根据执行环境正确配置它们。

The LRA business method

LRA 业务方法是我们定义业务逻辑的地方。

我们将把调用组织到其他微服务中,实现以下步骤:

  1. Create the offer for the desired football player, and set its status to "SEND".
  2. Retrieve the football player data.
  1. Set the status of the football player to "Reserved"; in this phase, the player is in negotiation, and therefore, cannot be sought after by other football managers.
  2. Persist the "Reserved" intermediate status, and mark the record with the LRA ID to be associated with this LRA context.
  3. Simulate the outcome of the negotiation with a check of the value of the offer made; if it is less than 80% of the value of the football player, the offer will be refused, and the LRA coordinator will execute the compensate phase. Otherwise, it will be accepted, and the LRA coordinator will execute the complete phase:
/**
* Business method that performs the send offer business logic.
* The method implement the business logic related to create the football
* player offer domain record and sets the status to a non final status, "SEND".
* It also set the status of the football player to Reserved.
* The LRA coordinator, based on the Response.Status, will decide how to
* complete or compensate the business logic setting the definitive status.
*
* @param footballPlayerOffer The POJO that maps the football player offer data.
*/
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
@LRA(value = LRA.Type.REQUIRED,
cancelOn = {Response.Status.INTERNAL_SERVER_ERROR}, // cancel on a 500 code
cancelOnFamily = {Response.Status.Family.CLIENT_ERROR} // cancel on any 4xx code)
public void sendOffer(FootballPlayerOffer footballPlayerOffer) {
LOG.log(Level.INFO, "Start method sendOffer");
LOG.log(Level.FINE, "Retrieving football player with id {0}",
footballPlayerOffer.getIdFootballPlayer());

String lraIdUrl = lraClient.getCurrent().toString();
String lraId = lraIdUrl.substring(lraIdUrl.lastIndexOf('/') + 1);

LOG.log(Level.FINE, "Value of LRA_ID {0}", lraId);

// Create the offer
LOG.log(Level.FINE, "Creating offer ...");
footballPlayerOffer.setStatus("SEND");
footballPlayerOffer.setLraId(lraId);
Response response = footballPlayerOfferTarget.request().post(Entity.entity(
footballPlayerOffer, MediaType.APPLICATION_JSON_TYPE));

LOG.log(Level.FINE, "Offer created with response code {0}", response.getStatus());

if (response.getStatus() == Response.Status.NO_CONTENT.getStatusCode()) {
FootballPlayer player = footballPlayerTarget.path(footballPlayerOffer.
getIdFootballPlayer().toString()).
request().get(new GenericTypeFootballPlayerImpl());

LOG.log(Level.FINE, "Got football player {0}", player);

player.setStatus("Reserved");
player.setLraId(lraId);

LOG.log(Level.FINE, "Changing football player status ...");

footballPlayerTarget.path(footballPlayerOffer.
getIdFootballPlayer().toString()).request().put(Entity.
entity(player, MediaType.APPLICATION_JSON_TYPE));

// Check about the price of the offer: if it is less than 80% of the
// value of the football player I will refuse the offer
BigInteger price = footballPlayerOffer.getPrice();

LOG.log(Level.FINE, "Value of offer price {0}", price);
LOG.log(Level.FINE, "Value of football player price {0}", player.getPrice());

if ((price.multiply(new BigInteger("100")).divide(player.getPrice())).intValue() < 80) {
throw new WebApplicationException("The offer is unacceptable!",
Response.Status.INTERNAL_SERVER_ERROR);
}
} else {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
LOG.log(Level.INFO, "End method sendOffer");
}

我们定义了LRA.Type.REQUIRED的值;这意味着,如果在 LRA 上下文之外调用它,则 JAX-RS 过滤器将在方法调用期间开始一个新的 LRA;并且,当调用完成时,另一个 JAX-RS 过滤器将完成 LRA。 该方法的关键点是使用了处理LRA生命周期的@LRA注解。

我们还定义了可以认为业务方法未能通过 cancelOn = {Response.Status.INTERNAL_SERVER_ERROR} 指示 LRA 协调器执行补偿阶段的情况,这会取消对 500 代码和 cancelOnFamily = {Response.Status.Family.CLIENT_ERROR},它取消任何 4xx 代码。

The complete phase

在完成阶段,LRA 协调器调用使用 @Complete 注释的方法,并根据数据状态执行使业务操作一致所需的业务操作。

在我们的场景中,该方法会将报价的状态设置为 "ACCEPTED",将足球运动员的状态设置为 "Purchased"

执行此操作需要以下代码:

/**
* LRA complete method: it sets the final status of the football player offer
* and football player based on a successful response of the send offer
* method.
*
* @param lraId The Long Running Action identifier needed to retrieve
* the record on which perform the operation.
*/
@PUT
@Path("/complete")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Complete
public void confirmOffer(@HeaderParam(LRAClient.LRA_HTTP_HEADER) String lraId) {
LOG.log(Level.INFO, "Start method confirmOffer: I'm in LRA complete phase");
LOG.log(Level.FINE, "Value of header lraId {0}", lraId);

String lraIdParameter = lraId.substring(lraId.lastIndexOf('/') + 1);

LOG.log(Level.FINE, "Value of lraIdParameter {0}", lraIdParameter);

// Set the offer to accepted
LOG.log(Level.FINE, "Setting the offer as ACCEPTED ...");
FootballPlayerOffer fpo = footballPlayerOfferTarget.path("lraId/" +
lraIdParameter).request().get(new GenericTypeFootballPlayerOfferImpl());

fpo.setStatus("ACCEPTED");

footballPlayerOfferTarget.path(fpo.getId().toString()).request().put(
Entity.entity(fpo, MediaType.APPLICATION_JSON_TYPE));

LOG.log(Level.FINE, "Set the offer as ACCEPTED ...");

// Set the football player status to purchased
FootballPlayer player = footballPlayerTarget.path("lraId/" + lraIdParameter).request().get(new GenericTypeFootballPlayerImpl());

LOG.log(Level.FINE, "Got football player {0}", player);

player.setStatus("Purchased");
player.setLraId(null);

LOG.log(Level.FINE, "Changing football player status ...");

footballPlayerTarget.path(player.getId().toString()).request().put(
Entity.entity(player, MediaType.APPLICATION_JSON_TYPE));

LOG.log(Level.INFO, "End method confirmOffer: LRA complete phase terminated"); }

该方法的主要元素是@Complete注解;这样,该方法就被认为是LRA的参与者,在启动LRA上下文的操作成功的情况下,LRA协调器会自动调用它。

The compensate phase

在补偿阶段,LRA 协调器调用带有 @Compensate 注释的方法,并根据确定操作失败的条件后的数据状态,执行使业务操作一致所需的业务操作.

在我们的场景中,该方法将提供的状态设置为 "REFUSED",足球运动员的状态设置为 "Free"。我们决定设置 "Free" 而不是 null,以表明记录是由 LRA 协调器调用的方法更新的。

执行此操作需要以下代码:

/**
* LRA compensate method: it sets the final status of the football player offer
* and football player based on a failed response of the send offer method.
*
* @param lraId The Long Running Action identifier needed to retrieve
* the record on which perform the operation.
* @return the Response of the operation.
*/
@PUT
@Path("/compensate")
@Produces(MediaType.APPLICATION_JSON)
@Compensate
public Response compensateWork(@HeaderParam(LRAClient.LRA_HTTP_HEADER) String lraId) {

LOG.log(Level.INFO, "Start method compensateWork: I'm in LRA compensate phase");

String lraIdParameter = lraId.substring(lraId.lastIndexOf('/') + 1);

LOG.log(Level.FINE, "Value of lraIdParameter {0}", lraIdParameter);

LOG.log(Level.FINE, "Setting the offer as REFUSED ...");
// Set the offer to REFUSED
FootballPlayerOffer fpo = footballPlayerOfferTarget.path("lraId/"
+ lraIdParameter).request().get(
new GenericTypeFootballPlayerOfferImpl());

fpo.setStatus("REFUSED");

footballPlayerOfferTarget.path(fpo.getId().toString()).request().put(
Entity.entity(fpo, MediaType.APPLICATION_JSON_TYPE));

LOG.log(Level.FINE, "Set the offer as REFUSED ...");

FootballPlayer player = footballPlayerTarget.path("lraId/"
+ lraIdParameter).
request().get(new GenericTypeFootballPlayerImpl());

LOG.log(Level.FINE, "Got football player {0}", player);

player.setStatus("Free");
player.setLraId(null);

LOG.log(Level.FINE, "Changing football player status ...");

footballPlayerTarget.path(player.getId().toString()).request().put(
Entity.entity(player, MediaType.APPLICATION_JSON_TYPE));

LOG.log(Level.INFO, "End method compensateWork: LRA compensate phase terminated");

return Response.ok().build();
}

该方法的主要元素是@Compensate注解;这样,该方法就被认为是LRA的参与者,并且在启动LRA上下文的操作失败的情况下,LRA协调器将自动调用它。

要运行微服务,您应该启动以下命令:

$ java -jar target/football-player-market-microservice-lra-thorntail.jar -Dlra.http.port=8580   

Football_Player_Market_client

这个模块是一个简单的 Java 客户端,它是为了测试我们的场景而创建的。

您可以指定两个参数——您要购买的足球运动员的 ID,以及您的报价的价值。如果不指定这些参数,默认值将分别为 1 (Gianlugi Buffon) 和 2200 万。

现在,是时候测试它了。运行以下命令:

$ java -jar target/Football_Player_Market_client-1.0.0.jar 18 1   

我出价 18 百万尝试购买 Gianluigi Buffon;我的报价将被视为有效,因为 1800 万是玩家价值的 80% 以上。因此,Narayana LRA 协调员将执行完整阶段。

以下是 football-player-market-microservice-lra 的摘录,显示了 LRA 协调员针对微服务执行的工作流:

2018-08-24   17:32:47,851 INFO    [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST]   (default task-1) Start method sendOffer
2018-08-24 17:32:47,851 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Retrieving football player with id 3
2018-08-24 17:32:47,853 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Value of LRA_ID 0_ffff7f000001_-595eb3c1_5b80245b_17
2018-08-24 17:32:47,853 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Creating offer ...
2018-08-24 17:32:48,452 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Offer created with response code 204
2018-08-24 17:32:48,874 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Got football player FootballPlayer{id=3, name=Keylor, surname=Navas, age=31, team=Real Madrid, position=goalkeeper, price=18, status=null, lraId=null}
2018-08-24 17:32:48,874 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Changing football player status ...
2018-08-24 17:32:49,018 INFO [stdout] (default task-1) Value of offer price: 18
2018-08-24 17:32:49,018 INFO [stdout] (default task-1) Value of football player price: 18
2018-08-24 17:32:49,018 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Value of offer price 18
2018-08-24 17:32:49,018 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) Value of football player price 18
2018-08-24 17:32:49,018 INFO [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-1) End method sendOffer
2018-08-24 17:32:49,131 INFO [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Start method confirmOffer: I'm in LRA complete phase
2018-08-24 17:32:49,132 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Value of header lraId http://localhost:8580/lra-coordinator/0_ffff7f000001_-595eb3c1_5b80245b_17
2018-08-24 17:32:49,132 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Value of lraIdParameter 0_ffff7f000001_-595eb3c1_5b80245b_17
2018-08-24 17:32:49,132 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Setting the offer as ACCEPTED ...
2018-08-24 17:32:49,306 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Set the offer as ACCEPTED ...
2018-08-24 17:32:49,335 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Got football player FootballPlayer{id=3, name=Keylor, surname=Navas, age=31, team=Real Madrid, position=goalkeeper, price=18, status=Reserved, lraId=0_ffff7f000001_-595eb3c1_5b80245b_17}
2018-08-24 17:32:49,335 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) Changing football player status ...
2018-08-24 17:32:49,353 INFO [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-2) End method confirmOffer: LRA complete phase terminated

我已经突出显示了显示业务方法的开始和结束以及由 LRA 协调员组织的完整阶段的日志消息。

如果你在数据库端执行检查,你会注意到 Gianluigi Buffon 的状态是 "purchased"

$ SELECT * FROM   football_player where id = 1

上述命令的输出如下:

id  name     surname age        team           position    price  
1  Gianluigi Buffon  40   Paris Saint Germain  goalkeeper    2 

status lraid
Purchased ""

如果您检查足球运动员的报价,如下所示,您将能够验证状态是 ACCEPTED

$ SELECT * FROM FOOTBALL_PLAYER_OFFER WHERE ID_FOOTBALL_PLAYER = 1

上述命令的输出如下:

ID ID_FOOTBALL_PLAYER ID_FOOTBALL_MANAGER PRICE STATUS
1 1 1 18 ACCEPTED

现在,我们将模拟一个故障场景。

我将出价5百万尝试购买曼努埃尔·诺伊尔;我的报价将被视为无效,因为 5 百万还不到玩家价值的 80%。因此,Narayana LRA 协调员将执行补偿阶段。

让我们运行以下命令:

$ java -jar target/Football_Player_Market_client-1.0.0.jar 5 2

以下是 football-player-market-microservice-lra 的摘录,显示了 LRA 协调员针对微服务执行的工作流:

2018-08-25 00:18:20,007 INFO [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Start method sendOffer
2018-08-25 00:18:20,008 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Retrieving football player with id 2
2018-08-25 00:18:20,008 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Value of LRA_ID 0_ffff7f000001_205e9dee_5b808159_53
2018-08-25 00:18:20,008 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Creating offer ...
2018-08-25 00:18:20,104 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Offer created with response code 204
2018-08-25 00:18:20,105 INFO [org.apache.http.impl.execchain.RetryExec] (default task-4) I/O exception (org.apache.http.NoHttpResponseException) caught when processing request to {}->http://localhost:8080: The target server failed to respond
2018-08-25 00:18:20,106 INFO [org.apache.http.impl.execchain.RetryExec] (default task-4) Retrying request to {}->http://localhost:8080
2018-08-25 00:18:20,113 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Got football player FootballPlayer{id=2, name=Manuel, surname=Neuer, age=32, team=Bayern Munchen, position=goalkeeper, price=35, status=null, lraId=null}
2018-08-25 00:18:20,113 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Changing football player status ...
2018-08-25 00:18:20,129 INFO [stdout] (default task-4) Value of offer price: 5
2018-08-25 00:18:20,129 INFO [stdout] (default task-4) Value of football player price: 35
2018-08-25 00:18:20,129 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Value of offer price 5
2018-08-25 00:18:20,129 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-4) Value of football player price 35
2018-08-25 00:18:20,130 ERROR [org.jboss.resteasy.resteasy_jaxrs.i18n] (default task-4) RESTEASY002010: Failed to execute: javax.ws.rs.WebApplicationException: The offer is unacceptable!
at com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST.sendOffer(FootballPlayerMarketREST.java:142)
at com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST$Proxy$_$$_WeldClientProxy.sendOffer(Unknown Source)
...
2018-08-25 00:18:20,152 INFO [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) Start method compensateWork: I'm in LRA compensate phase
2018-08-25 00:18:20,152 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) Value of lraIdParameter 0_ffff7f000001_205e9dee_5b808159_53
2018-08-25 00:18:20,153 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) Setting the offer as REFUSED ...
2018-08-25 00:18:20,196 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) Set the offer as REFUSED ...
2018-08-25 00:18:20,203 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) Got football player FootballPlayer{id=2, name=Manuel, surname=Neuer, age=32, team=Bayern Munchen, position=goalkeeper, price=35, status=Reserved, lraId=0_ffff7f000001_205e9dee_5b808159_53}
2018-08-25 00:18:20,203 FINE [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) Changing football player status ...
2018-08-25 00:18:20,227 INFO [com.packtpub.thorntail.footballplayermicroservice.rest.service.FootballPlayerMarketREST] (default task-5) End method compensateWork: LRA compensate phase terminated

我已经突出显示了显示业务方法的开始和结束以及由 LRA 协调员组织的补偿阶段的日志消息。

如果您在数据库端执行检查,您会注意到 Manuel Neuer 的 statusFree

$ java -jar target/Football_Player_Market_client-1.0.0.jar 5 2

上述命令的输出如下:

id name surname age     team       position  price status lraid 
2  Manuel Neuer  32 Bayern Munchen goalkeeper 35   Free    "" 

如果您查看足球运动员的报价,您可以验证状态是否为 REFUSED

$ SELECT * FROM FOOTBALL_PLAYER_OFFER WHERE ID_FOOTBALL_PLAYER = 2 

上述命令的输出如下:

2 2 1 5 REFUSEDID ID_FOOTBALL_PLAYER ID_FOOTBALL_MANAGER PRICE STATUS

Limitations

如您所见,LRA 规范有很大的潜力;它可以帮助管理微服务架构中数据的一致性。

在撰写本书时,该规范仍处于草案形式,还有许多需要改进的地方。

在回顾了我们应用程序的用例之后,我认为必须解决以下重要限制:

  • Single point of failure: In the case of Narayana implementation, there is no way to set the LRA coordinator in high availability. The object store that it used to store the LRA information cannot be shared between instances, so unavailability of the LRA coordinator could be a severe issue. I think that this problem will be overcome quickly, since the Narayana team is working to provide scalability and failover for the coordinator.
  • Passing parameters to the @Compensate and @Complete methods is very complicated: For this reason, in our scenario, I used a workaround and stored the LRA ID in the domain model to perform operations against the right data. This problem makes the implementation a little more complicated.

Summary

在本章中,我们讨论了在微服务架构中实现和处理事务;特别是,我们介绍了 ACID 原则、XA 事务、应用程序补偿和 saga 模式。我们实现了一个使用 Narayana 作为 LRA 规范的实现来处理事务的示例。

在下一章中,我们将讨论容器技术,以及它如何帮助开发人员构建微服务基础设施。