Axon Framework官方文档(七)

7. Managing complex business transactions(管理复杂业务事务)

不是每个命令都能在一个ACID事务中完全执行。现金转账事务是一个很常见频繁出现的例子,用来作为论据。人们通常认为,从一个帐户转到另一个帐户是绝对需要一个原子和一致的事务的。嗯,它不是。相反,这是完全不可能的。如果钱从A银行的账户转到B银行的另一个账户,该怎么办?银行A是否在B银行的数据库中获得了一个锁?如果转账正在进行的时候,A银行已经扣除了金额,但B银行并没收到它,这不是很奇怪吗?事实上不是,这是“正在进行”。另一方面,如果在向B银行的帐户中存资金时出现错误,A银行的客户就想要他的钱回退。所以我们需要某种形式的最终一致性。
虽然在某些情况下,ACID事务不是必需的,甚至是不可能的,但仍然需要某种形式的事务管理。通常,这些事务被称为BASE事务:基本可用性、软状态、最终一致性。与ACID相反,BASE事务不容易回滚。为了回滚,需要采取补偿操作来还原作为事务的一部分发生的任何东西。在转账的例子中,银行B银行存款的失败,将退还银行A的钱。
在CQRS中,Sagas可用于管理这些基本事务。它们对事件做出响应,并可能分派命令、调用外部应用程序等。在领域驱动设计的上下文中,Sagas作为多个有界上下文之间的协调机制并不少见。

7.1 Saga

saga是一种特殊类型的事件监听器:用来管理业务事务。有些事务可以运行数日甚至数周,而另一些事务则在几毫秒内完成。在Axon中,saga的每个实例负责管理一个业务事务。这意味着saga维护了管理该事务的必要状态,继续或采取补偿行动来撤销已经采取的任何行动。通常情况下,与常规事件侦听器相反,一个事件有一个起点和一个结束,都是由事件触发的。虽然saga的起点通常是非常明确的,但可能有多种方式结束一个saga。
在Axon中,Sagas是定义了一个或多个@SagaEventHandler方法的类。与常规事件处理程序不同,在任何时间可能存在多个saga的实例。Saga由一个Processor管理(跟踪或订阅),它致力于为指定类型的saga处理事件。

7.2 Life Cycle

单个saga实例负责管理单个事务。这意味着你需要能够指出saga的生命周期的开始和结束。
在一个saga中,事件处理器用@SagaEventHandler进行注解。如果某个特定事件表示事务的开始,则在同一方法中添加另一个注释:@StartSaga。这个注释将创建一个新的事件,并在发布了与其相匹配事件时调用它的事件处理程序方法。
默认情况下,只有在没有合适的已存在的saga(相同类型)的情况下才会创建一个新的saga。您还可以通过在@StartSaga注解上设置forceNew属性来强制创建一个新的事件实例。
结束一个saga可以用两种方式完成。如果某一事件总是预示着一个saga的生命周期的结束,那么就用@EndSaga注解事件的处理器。saga的生命周期将在处理程序的调用结束后结束。或者,您可以从saga中调用end()来结束生命周期。这让你可以条件性的结束一个saga。

7.3 Event Handling(事件处理)

saga的事件处理器与常规事件侦听器相当。方法和参数解析的相同规则在这里是有效的。不过,有一个主要的区别。虽然事件侦听器的单个实例可以处理所有传入事件,但可能存在多个saga实例,每个实例都对不同的事件感兴趣。例如,一个管理id为"1"的Order的事务的saga,将不会对id为"2"的Order的事件感兴趣,反之亦然。
Axon不会将所有的事件,都发往到所有的saga实例上(因为这很浪费资源。可想而知,如果那么做,每个saga都要处理自己不感兴趣的事件,对其过滤),而是发布包含与saga相关联的属性的事件。这是通过AssociationValues来实现的。AssociationValue 包含一个key和一个value,key代表标识符使用的类型,例如“orderId”或“order”。value表示前面例子中相应“1”或“2”值。
@SagaEventHandler注解的方法被评估的顺序与@EventHandler方法相同(见注解事件处理程序,即官方文档六)。如果处理器方法的参数与传入事件匹配,且事件与处理器中定义的属性有关联,则方法被匹配。
@SagaEventHandler注解有两个属性,其中associationProperty 属性是最重要的。这是传入事件中某个属性的名称,该属性被用于查找与该事件相关的Sagas。association值的key是property的名称。这个值是由property的getter方法返回的值。
例如,考虑一个带”String getOrderId()”方法的传入事件,返回“123”。如果一个带@SagaEventHandler(associationProperty = orderId)注解的方法接受这个事件,这个事件被路由到所有已经与带一个key为orderId和value为“123”的AssociationValues相关联的saga。这可能是一个,多个,甚至没有。
有时,想要关联的属性的名称不是想要使用的关联的名称。例如,你有一个销售订单相匹配购买订单的saga。你可以有一个包含“buyOrderId”和“sellOrderId”的事务对象。如果你想要的saga将“orderId”作为关联的值,你可以定义一个不同的keyName 在@SagaEventHandler注解中。它将变成@SagaEventHandler(associationProperty="sellOrderId", keyName="orderId")。

7.4 Managing associations

当一个saga在多个领域概念(如订单、装运、发票等)管理事务时,该saga需要与这些概念的实例相关联。关联需要两个参数:key,它标识关联的类型(Order、Shipment等)和一个值,该值表示该概念的标识符。
将一个saga与一个概念联系在一起是有多种方式的。首先,当在调用@StartSaga注解的事件处理器时,新的saga被创建,它会自动关联到@ SagaEventHandler方法中标识的属性。任何其他关联都可以使用SagaLifecycle.associateWith(String key, String/Number value))方法来创建。使用SagaLifecycle.removeAssociationWith(String key, String/Number value)方法来删除一个特定的关联。
想象一下为一个围绕着订单的事务而已经被创建的一个saga。saga自动关联订单,因为方法被@StartSaga注解了。该事件负责为该订单创建发票,并告诉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
        associateWith("shipmentId", shipmentId);
        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) { end(); }
    }

    @SagaEventHandler(associationProperty = "invoiceId")
    public void handle(InvoicePaidEvent event) {
        paid = true;
        if (delivered) { end(); }
    }

    // ...
}
通过允许客户端生成标识符,可以轻松地将一个saga关联到一个概念,而不需要请求-响应类型命令。在发布命令之前,我们将事件与这些概念关联起来。这样,我们保证也可以捕获作为该命令一部分生成的事件。一旦发票支付,货物到达,那么这个saga也将结束。

7.5 Keeping track of Deadlines(保持对截止日期的追踪)

当有事发生时,很容易使一个saga采取行动。毕竟,有一个事件可以通知saga。但是如果你想让你的saga,在什么事情都没发生的情况下做些什么呢?这就是最后期限的用途。在发票中,这通常是几个星期,而信用卡付款的确认应该在几秒钟内发生。
在Axon中,您可以使用EventScheduler来调度发布的事件。在发票的例子中,你希望发票在30天内付款。saga将发送CreateInvoiceCommand后,会在30天时候调度一个InvoicePaymentDeadlineExpiredEvent事件。EventScheduler在调度事件后返回ScheduleToken。这个令牌可以用来取消调度,例如当收到发票付款时。
Axon提供了两个EventScheduler实现:一个纯Java的实现,一个使用Quartz 2作为支持调度机制。
此EventScheduler的纯java实现使用ScheduledExecutorService来安排事件的发布。虽然这个调度程序的时间非常可靠,但是它是一个纯粹的内存实现。一旦JVM被关闭,所有的时间表都将丢失。这使得这种实现不适合长期的时间表。
SimpleEventScheduler需要配置一个EventBus和SchedulingExecutorService(参见java.util.concurrent.Executors的静态帮助方法)
QuartzEventScheduler是一个更加可靠和企业级的实现。使用Quartz作为基础的调度机制,它提供了更强大的特性,如持久性、集群和失败管理。这意味着事件发布是有保证的。可能会晚一点,但总会发布。
需要配置一个Quartz调度器和EventBus。另外,你可以为调度任务设置组名,默认为"AxonFramework-Events"。
一个或多个组件将监听调度的事件。这些组件可能依赖于绑定到调用它们的线程的事务。调度的事件由EventScheduler管理的线程发布。要在这些线程上管理事务,您可以配置一个TransactionManager或一个创建事务绑定的Unit of Work的UnitOfWorkFactory。
Note:
Spring用户可以使用QuartzEventSchedulerFactoryBean(持久化的)或者SimpleEventSchedulerFactoryBean(非持久化的),就可以很容易配置。它允许您直接设置PlatformTransactionManager。

7.7 Injecting Resources(注入资源)

Sagas通常不只是基于事件维护状态。它们还与外部组件交互。要做到这一点,他们需要访问处理组件所需的资源。通常情况下,这些资源并不是saga状态的一部分,也不应该持久化。但是一旦重建了一个saga,在事件被路由到该实例之前,必须注入这些资源。
为了这个目的,有了ResourceInjector。它被SagaRepository使用,将资源注入到一个saga中。Axon提供了SpringResourceInjector,他可以从应用上下文将资源注入到被注解标识的字段和方法上,和一个SimpleResourceInjector,将已注册的资源注入带@inject注解的方法和字段。
Tip:
由于资源不应该被持久化,所以一定要在这些字段中添加transient关键字。这将防止序列化机制尝试将这些字段的内容写入存储库。在一个事件被反序列化之后,存储库将自动重新注入所需的资源。
SimpleResourceInjector允许预先指定的资源集合被注入。它扫描了一个saga的(setter)方法和字段,以找到带有@inject注解的方法。
当使用配置API,Axon将默认为 ConfigurationResourceInjector。它将注入配置中可用的任何资源。组件像EventBus、EventStore CommandBus和CommandGateway默认情况下是可用的,但你也可以使用configurer.registerComponent()注册自己的组件。
SpringResourceInjector使用Spring的依赖注入机制将资源注入到聚合中。这意味着如果您需要的话,您可以使用setter注入或直接字段注入。要注入的方法或字段需要注解,以便Spring将其识别为依赖项,例如@ autowired。

7.8 Saga Infrastructure(saga基础设施)

事件需要重定向到适当的saga实例。为此,需要一些基础设施类。最重要的组成部分是SagaManager和SagaRepository。

7.8.1 Saga Manager(saga管理器)

与处理事件的任何组件一样,processing也是由Event Processor完成的。然而,由于Sagas并不是单例处理事件,而是有单独的生命周期,因此需要管理它们。
Axon通过AnnotatedSagaManager支持生命周期管理,它提供给一个Event Processor来执行实际的处理程序调用。它的初始化使用saga的类型来管理,也使用可以存储和恢复的SagaRepository这种saga类型。一个AnnotatedSagaManager只能管理一个saga类型。
在使用配置API时,Axon将对大多数组件使用合理的缺省值。然而,强烈建议定义一个SagaStore实现来使用。SagaStore是在物理上存储saga实例的机制。AnnotatedSagaRepository(默认)使用SagaStore来存储和检索需要的事件实例。
Configurer configurer = DefaultConfigurer.defaultConfiguration();
configurer.registerModule(
        SagaConfiguration.subscribingSagaManager(MySagaType.class)
                         // Axon defaults to an in-memory SagaStore, defining another is recommended
                         .configureSagaStore(c -> new JpaSagaStore(...)));

// alternatively, it is possible to register a single SagaStore for all Saga types:
configurer.registerComponent(SagaStore.class, c -> new JpaSagaStore(...));

7.8.2 Saga Repository and Saga Store

SagaRepository负责存储和检索Sagas,供SagaManager使用。它可以通过它们的标识符以及它们的关联值来检索特定的saga实例。
然而,有一些特殊的要求。由于Sagas中的并发处理是一个非常微妙的过程,所以存储库必须确保每个概念性的事件实例(具有相同的标识符)只有一个实例存在于JVM中。
Axon提供了带AnnotatedSagaRepository实现,它允许查找saga实例,同时保证在同一时间只有一个saga实例被访问。它使用SagaStore来执行saga实例的实际的持久性。
实现的选择主要取决于应用程序使用的存储引擎。Axon提供了JdbcSagaStore、InMemorySagaStore、JpaSagaStore和MongoSagaStore。
在某些情况下,应用程序从缓存saga实例中获益。在这种情况下,有一个CachingSagaStore,它封装了另一个实现来添加缓存行为。请注意,CachingSagaStore是一个write-through缓存,这意味着保存操作总是立即被转发到备份存储,以确保数据安全。
7.8.2.1 JpaSagaStore
JpaSagaStore使用JPA来存储Sagas的状态和关联值。Sagas本身不需要任何JPA注释;Axon将使用Serializer序列化sagas(类似于事件序列化,您可以使用JavaSerializer或XStreamSerializer)。
JpaSagaStore通过EntityManagerProvider来配置,它提供对EntityManager实例的访问。这种抽象允许使用应用程序管理的和容器管理的entitymanager。根据情况,你可以定义序列化器去序列化saga实例。Axon默认为XStreamSerializer。
7.8.2.2 JdbcSagaStore
JdbcSagaStore使用纯JDBC来存储saga实例及其关联值。与JpaSagaStore类似,saga实例不需要知道它们是如何存储的。它们使用序列化器进行序列化。
JdbcSagaStore由DataSource或ConnectionProvider初始化。虽然不是必需的,与ConnectionProvider初始化时,建议用UnitOfWorkAwareConnectionProviderWrapper来包装实现。它将检查当前已打开的数据库连接的工作单元,以确保在一个工作单元内的所有活动都是在单个连接上完成的。
与JPA不同,JdbcSagaRepository使用简单的SQL语句存储和检索信息。这可能意味着某些操作依赖于特定于数据库的SQL方言。也可能是某些数据库供应商提供了您想要使用的非标准特性。为此,您可以提供您自己的SagaSqlSchema。SagaSqlSchema是一个接口,它定义存储库需要在底层数据库上执行的所有操作。它允许您为每一个自定义执行的SQL语句。默认是GenericSagaSqlSchema。其他可用的实现是PostgresSagaSqlSchema、Oracle11SagaSqlSchema 、HsqlSagaSchema。
7.8.2.3 MongoSagaStore
MongoSagaStore在MongoDB数据库中存储saga实例及其关联值。MongoSagaStore将所有的sagas存储在MongoDB数据库中的单个集合中。为每个saga实例,创建一个文档(毕竟mongo就是文档数据库)。
MongoSagaStore还确保在任何时候,在单个JVM中只存在一个单独的saga实例。这确保了由于并发问题,不会出现任何的状态丢失。
MongoSagaStore是使用MongoTemplate和可选的Serializer来初始化的。MongoTemplate提供了对保存saga的集合的引用。Axon提供了DefaultMongoTemplate,它接收MongoClient实例以及数据库名称和集合的名称,来存储sagas。可以省略数据库名称和集合名。在这种情况下,它们分别默认为“axonframework”和“sagas”。
7.8.2.4 Caching
如果使用数据库支持的saga存储,保存和载入saga实例可能是一个相对昂贵的操作。特别是在同一个saga实例在短时间内多次被调用的情况下,缓存对应用程序的性能是有利的。
Axon 提供了CachingSagaStore实现。这个SagaStore包装实际的存储。当查询加载saga或关联值,在委托到封装的存储库之前,CachingSagaStore将首先查看其缓存。而当存储信息时,所有调用总是被委托,以确保后备存储器总是有一个与saga的状态一致的视图。
要配置缓存,只需将任何SagaStore包装成CachingSagaStore。CachingSagaStore的构造函数有三个参数:包装的存储库,分别用于关联值和saga实例的缓存。后两个参数可以引用相同的缓存,也可以引用不同的缓存。这取决于具体应用程序的回收需求。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值