Axon框架使用指南(五):管理复杂的业务事务

          并非每个命令都能够在单个ACID事务中完全执行。资金转移是一个非常常见的例子,资金常常作为交易的一个参数出现。人们经常认为,将资金从一个账户转移到另一个账户绝对需要原子和一致的交易。好吧,事实并非如此。相反,这是不可能的。如果资金从银行A的账户转移到银行B的另一个账户,该怎么办? A银行是否获得了B银行数据库的锁定?如果转帐正在进行中,银行A已扣除金额,但银行B尚未存入呢?另一方面,如果在将钱存入银行B的账户时出现问题,则银行A的客户会想要退还他的钱。所以我们确实期望一定的一致性。

         尽管在某些情况下ACID交易不是必要的,甚至不可能,但仍需要某种形式的交易管理。通常,这些事务被称为BASE事务:基本可用性,软状态,最终一致性。与ACID相反,BASE事务不能轻易回滚。为了回滚,需要采取补偿措施来恢复作为交易一部分所发生的任何事情。在汇款的例子中,B银行存款失败将退还银行A的款项。

         在CQRS中,Sagas可以用来管理这些BASE事务。他们对事件做出响应并能调度命令,调用外部应用程序等。在领域驱动设计的背景下,Sagas被用作几个限界上下文之间的协调机制,这并不罕见。

Saga

       Saga是一种特殊类型的事件监听器:一种管理业务事务的事件监听器。 一些事务可能会持续几天甚至几周,而其他事务则会在几毫秒内完成。 在Axon中,Saga的每个实例都负责管理单个业务事务。 这意味着Saga保持管理该事务所需的状态,继续执行或采取补偿行动以回滚已采取的任何行动。 通常情况下,与常规事件监听器相反,Saga有起点和终点,都是由事件触发的。 虽然Saga的出发点通常很清楚,但Saga可以有很多方式结束。

在Axon中,Sagas是定义一个或多个@SagaEventHandler方法的类。 与常规事件处理程序不同,Saga的多个实例可能随时存在。Sagas由一个处理器(追踪或订阅)管理,该处理器专用于处理该特定Saga类型的事件。

生命周期

        单个Saga实例负责管理单个事务。这意味着您需要能够指示Saga的生命周期的开始和结束。

在Saga中,事件处理程序使用@SagaEventHandler进行注释。如果特定的事件表示事务的开始,则向该方法添加另一个注释:@StartSaga。此注释将创建一个新的Saga,并在发布匹配的事件时调用其事件处理方法。

       默认情况下,只有当没有合适的已有Saga(同类型)可以找到时,才会启动新的Saga。您还可以通过将@StartSaga注释中的forceNew属性设置为true来强制创建新的Saga实例。

      结束Saga可以通过两种方式完成。如果某个事件总是表示Saga的生命周期结束,请使用@EndSaga在Saga中注释该事件的处理程序。Saga的生命周期将在调用处理程序后结束。或者,您可以从Saga内部调用SagaLifecycle.end()来结束生命周期。这可以让你有条件地结束Saga。

事件处理

        Saga中的事件处理与常规的事件监听器一样。 方法和参数的解析规则在这里是有相同的, 但是有一个主要区别。 虽然有一个EventListener实例可处理所有传入事件,但可能存在多个Saga实例,每个实例都对不同的事件实例感兴趣。 例如,管理ID为“1”的订单相关交易的Saga将不会对订单“2”的事件感兴趣,反之亦然。

       Axon不会将所有事件发布到所有Saga实例(这将完全浪费资源),而只会发布与Saga实例有关的Events给它。 这是使用AssociationValue完成的。AssociationValue由一个键和一个值组成。 键表示所用的标识符,例如“orderId”或“order”。 该值就是上例中的对应值“1”或“2”。

       评估@SagaEventHandler注释方法的顺序与@EventHandler方法的顺序相同(请参阅注解事件处理程序章节)。如果处理程序方法的参数与传入的事件匹配,并且该事件与处理程序方法中定义的属性有关联,则方法匹配。

       @SagaEventHandler注释具有两个属性,其中associationProperty是最重要的属性。这是传入的Event上的属性的名称,它用于查找关联的Sagas,关联的key是Event上的属性的名称。该值是该属性的getter方法返回的值。

      例如,考虑含有方法“String getOrderId()”的传入事件,该方法返回“123”。如果接受此事件的方法使用@SagaEventHandler(associationProperty=“orderId”)进行注解,那么将此事件路由到所有AssociationValue的key是“orderId”和值为“123”的Sagas处理方法。这可能只是一个,多于一个,甚至完全没有。

     有时,您想要关联的属性的名称不是要使用的关联的名称。例如,您有一个与卖出订单与买入订单相匹配的Saga,您可以拥有一个包含“buyOrderId”和“sellOrderId”的交易对象。如果您希望Saga将该值作为“orderId”关联,则可以在@SagaEventHandler批注中定义不同的keyName。它会被写成

@SagaEventHandler(associationProperty=“sellOrderId”,keyName =“orderId”)

管理关联

       将Saga与一个概念联系起来有几种方式。 首先,当调用带@StartSaga注释的事件处理程序时,会新创建了一个Saga,它会自动与@SagaEventHandler方法中标识的属性相关联。 可以使用该方法创建任何其他关联。使用SagaLifecycle.removeAssociationWith(String key,String /Number值)方法删除特定的关联。

        在Saga中关联领域概念的API有意识地仅允许字符串或数字作为标识值,因为存储的关联值条目需要标识符的字符串表示。 在API中使用简单的标识符值和直接的字符串表示是有目的的,因为数据库中的字符串列条目使数据库引擎之间的比较更简单。 因此,Saga没有associateWith(String,Object),因为Object的toString()调用的结果可能会提供错误的标识符。

        想象围绕订单的交易创建了一个Saga。 Saga与订单自动关联,因为该方法用@StartSaga注释。 Saga负责为该订单创建发票,并告诉Shipping为其创建货单。 一旦货单到达且发票已付款,交易即告完成,Saga关闭。

这就是这样的Saga的代码:

public class OrderManagementSaga {
   private boolean paid = false; 
   private boolean delivered = false; 
   @Inject
   private transient CommandGateway commandGateway;
   @StartSaga
   @SagaEventHandler(associationProperty = "orderId") public void handle(OrderCreatedEvent event) {
      // client generated identifiers
      ShippingId shipmentId = createShipmentId();
      InvoiceId invoiceId = createInvoiceId();
      //associate the Saga with these values, before sending the commands SagaLifecycle.associateWith("shipmentId", shipmentId); SagaLifecycle.associateWith("invoiceId", invoiceId);
      //send the commands
      commandGateway.send(new PrepareShippingCommand(...)); 
      commandGateway.send(new CreateInvoiceCommand(...));
   }
   @SagaEventHandler(associationProperty = "shipmentId")   
   public void handle(ShippingArrivedEvent event) {
     delivered = true;
     if (paid) { SagaLifecycle.end(); }
  }
  @SagaEventHandler(associationProperty = "invoiceId")
  public void handle(InvoicePaidEvent event) {
    paid = true;
    if (delivered) { 
     SagaLifecycle.end();
     }
  }
}

         通过允许客户端生成标识符,Saga可以很容易地与一个概念相关联,而不需要请求 - 响应类型的命令。 在发布命令之前,我们将事件与这些概念相关联。 这样,我们可以保证捕获此命令所生成的事件。 一旦支付了发票并且货物已经到达,这将结束这个Saga。

跟踪截止日期

        当事情发生时,让Saga采取行动很容易。毕竟,有一个事件通知Saga。但是如果你想让你的Saga在没有任何事情发生时也能做点什么呢?例如截止日期到达时。对于发票来说,可能通常要处理几个星期,而信用卡支付的确认应在几秒钟内发生。

       在Axon中,您可以使用EventScheduler来安排要发布的事件。在发票的例子中,您希望发票在30天内支付。在发送CreateInvoiceCommand之后,Saga会安排一个InvoicePaymentDeadlineExpiredEvent在30天内发布。EventScheduler在安排事件后返回一个ScheduleToken。此令牌可用于取消计划,例如收到发票付款时。

        Axon提供了两个EventScheduler实现:一个纯Java,一个使用Quartz2作为后台调度机制。

        EventScheduler的纯Java实现使用ScheduledExecutorService来安排Event发布。虽然这个调度程序的时间非常可靠,但它是一个纯粹的内存中实现。一旦JVM关闭,所有日程安排都将丢失。这使得这种实现不适用于长期计划。

   SimpleEventScheduler需要使用EventBus和SchedulingExecutorService进行配置(请参阅java.util.concurrent.Executors类的静态方法以查看方法的帮助)。

      QuartzEventScheduler是一个更可靠且适用于企业的实现。 使用Quartz作为基础调度机制,它提供了更强大的功能,如持久性,集群和灾备管理。 这意味着事件发布是有保证的。 它可能有点延迟,但一定会发布。

    它需要配置一个QuartzScheduler和一个EventBus。另外,您可以设置Quartz作业计划的组名,默认为“AxonFramework-Events”。一个或多个组件将监听预定的事件。 这些组件可能依赖于绑定到调用它们的线程的事务。计划事件由EventScheduler管理的线程发布, 要管理这些线程上的事务,您可以配置一个TransactionManager或一个用于创建事务并绑定到工作单元的UnitOfWorkFactory。

      注意:Spring用户可以使用QuartzEventSchedulerFactoryBean或SimpleEventSchedulerFactoryBean进行更简单的配置。 它允许您直接设置PlatformTransactionManager。

注入资源

       Sagas通常不仅仅是基于事件维护状态。 他们也与外部组件进行交互。 为此,他们需要访问必要的资源来查找组件。 通常情况下,这些资源实际上并不是Saga状态的一部分,因此不应该同样被持久化,但是一旦一个Saga实例被重新创建,这些资源就必须在事件发送到该Saga实例之前注入。为此,就有了ResourceInjector。 它被SagaRepository用于向Saga注入资源。 Axon提供了一个SpringResourceInjector。

      Axon提供了一个SpringResourceInjector,它从应用程序上下文中获取资源并注入带注解的字段和方法,以及一个SimpleResourceInjector,它将已注册到它内部的资源注入到带有@Inject注解的方法和字段。

       注意:由于资源不应该与Saga一起持久化,请务必将transient关键字添加到这些字段。 这将阻止序列化机制尝试将这些字段的内容写入存储库。在Saga被反序列化后,存储库将自动重新注入所需的资源。

       SimpleResourceInjector允许注入预先指定的资源集合。 它扫描Saga的setter方法和字段,找到用@Inject注释的方法和字段。使用ConfigurationAPI时,Axon将默认使用ConfigurationResourceInjector。 它将注入配置中可用的任何资源。 像EventBus,EventStore,CommandBus和CommandGateway等组件默认是可用的,但您也可以使用configurer.registerComponent()注册您自己的组件。SpringResourceInjector使用Spring的依赖注入机制将资源注入到聚合中。 这意味着如果需要,您可以使用setter注射或直接field注入。 要注入的方法或字段需要注释,以便Spring将其识别为依赖项,例如使用@Autowired

Saga 管理

        与处理事件的任何组件一样,处理由事件处理器完成。但是,由于Sagas不是处理事件的单例,而是具有单独的生命周期的不同个体,因此需要对其进行管理。Axon通过AnnotatedSagaManager支持Saga生命周期管理,AnnotatedSagaManager提供给事件处理器来执行对处理程序的实际调用。它使用Saga的类型进行初始化,以及可以存储和检索该类型的Sagas的SagaRepository。 一个AnnotatedSagaManager只能管理一个单一的Saga类型。使用配置API时,Axon将为大多数组件使用合理的默认值。 不过,强烈建议您定义要使用的SagaStore实现。该SagaStore是“物理”存储Saga实例的基础设施。默认通过AnnotatedSagaRepository使用SagaStore根据需要存储和检索Saga实例。 

// Axon defaults to an in-memory SagaStore, defining another is recommended
SagaConfiguration.subscribingSagaManager(MySagaType.class).configureSagaStore(c -> new JpaSagaStore(...)));
// alternatively, it is possible to register a single SagaStore for all Saga types: 
configurer.registerComponent(SagaStore.class, c -> new JpaSagaStore(...));

Saga仓储和Saga存储库

         SagaRepository负责存储和检索Sagas,供SagaManager使用。 它能够通过它们的标识符以及它们的关联值来检索特定的Saga实例。但是,有一些特殊要求。 由于Sagas中的并发处理是一个非常微妙的过程,因此存储库必须确保概念上相同的Saga实例(具有相同的标识符)在JVM中只存在一个实例。Axon提供了AnnotatedSagaRepository实现,该实现允许查找Saga实例,同时保证只有一个Saga实例同时被访问。 它使用SagaStore来执行Saga实例的实际持久化。实现的选择主要取决于应用程序使用的存储引擎。 Axon提供了JdbcSagaStore,InMemorySagaStore,JpaSagaStore和MongoSagaStore。

        在某些情况下,应用程序可从缓存Saga实例中受益。 在那种情况下,通过一个CachingSagaStore包装另一个SagaStore实现来添加缓存行为。 请注意,CachingSagaStore是一个直接写入的缓存,这意味着保存操作总是会立即转发到后台存储,以确保数据安全。

JpaSagaStore

       JpaSagaStore使用JPA来存储Saga的状态和关联值。 Sagas本身不需要任何JPA注释; Axon将序列化Saga(类似于事件序列化,您可以使用JavaSerializerXStreamSerializer)。

       JpaSagaStore配置了一个EntityManagerProvider,该EntityManagerProvider提供对EntityManager实例的访问。该抽象允许使用应用程序管理的和容器管理的EntityManagers。可选地,您可以定义序列化器以序列化Saga实例。 Axon默认为XStreamSerializer。

JdbcSagaStore

        JdbcSagaStore使用普通的JDBC来存储Saga的阶段及Saga关联值。与JpaSagaStore类似,Saga实例不需要知道它们的存储方式。Saga实例被Serializer进行序列化。

        JdbcSagaStore使用DataSource或ConnectionProvider进行初始化。虽然不是必需的,但在使用ConnectionProvider进行初始化时,建议将实现包装在UnitOfWorkAwareConnectionProviderWrapper中。它将检查当前的UnitOf Work是否已经打开了数据库连接,以确保Unit Of Work内的所有活动都在单个连接上完成。

        与JPA不同的是,JdbcSagaRepository使用普通的SQL语句来存储和检索信息。这可能意味着某些操作取决于数据库特定的SQL方言,也可能取决于某些数据库供应商提供的非标准功能。为了适应这一点,你可以提供你自己的SagaSqlSchema。 SagaSqlSchema是一个接口,用于定义存储库需要在底层数据库上执行的所有操作。它允许您自定义为它们中的每一个执行的SQL语句。默认值是GenericSagaSqlSchema。其他可用的实现有PostgresSagaSqlSchema,Oracle11SagaSqlSchema和HsqlSagaSchema

MongoSagaStore

        MongoSagaStore将Saga实例及其关联存储在MongoDB数据库中。MongoSagaStore将所有Saga都存储在MongoDB数据库中的单个集合中。 每个Saga实例创建单个文档。MongoSagaStore还可以确保在任何时候,对于同一个标识符的Saga,单个JVM中只有一个Saga实例。 这可以确保没有状态更改会由于并发的问题而丢失。MongoSagaStore使用MongoTemplate和可选的Serializer进行初始化。TheMongoTemplate提供了一个对存储Sagas的集合的引用.Axon提供了DefaultMongoTemplate,它接受MongoClient实例以及集合名称和数据库名称以存储Saga。数据库名称和集合名称可以省略。 在这种情况下,他们分别默认为“axonframework”和“sagas”。

缓存

        如果使用支持数据库的Saga存储,保存和加载Saga实例可能是一个相对昂贵的操作。特别是在短时间跨度内多次调用同一个Saga实例的情况下,缓存可以有利于应用程序的性能。

      Axon提供了CachingSagaStore实现。这是一个SagaStore包装另一个实际的存储。在加载Sagas或关联值时,CachingSagaStore在将操作委派给实际存储库之前首先查询其高速缓存。存储信息时,所有调用都会被委派给实际存储,以确保后备存储始终对Saga状态具有一致的视图。

        要配置缓存,只需将任何SagaStore包装在CachingSagaStore中即可。

       CachingSagaStore的构造函数有三个参数:要包装的存储库、用于关联值的缓存和用于Saga实例的缓存。后两个参数可能指向相同的缓存或不同的缓存。这取决于您的特定应用程序的要求。

猜你喜欢

转载自blog.csdn.net/wangli13860426642/article/details/80668429
今日推荐