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
    评论
1.了解Spring 2.了解NoSQL和文档数据库 3.要求 4.其他帮助资源 4.1。支持 4.1.1。社区论坛 4.1.2。专业支持 4.2。发展之后 5.新&值得注意的 5.1。Spring Data MongoDB 2.1中的新特性 5.2。Spring Data MongoDB 2.0中的新特性 5.3。Spring Data MongoDB 1.10中的新特性 5.4。Spring Data MongoDB 1.9中的新特性 5.5。Spring Data MongoDB 1.8中的新特性 5.6。Spring Data MongoDB 1.7中有什么新功能 6.依赖 6.1。Spring Boot的依赖管理 6.2。Spring框架 7.使用Spring Data Repositories 7.1。核心概念 7.2。查询方法 7.3。定义存储库接口 7.3.1。微调储存库定义 7.3.2。空处理存储库方法 7.3.3。将存储库与多个Spring Data模块一起使用 7.4。定义查询方法 7.4.1。查询查询策略 7.4.2。查询创建 7.4.3。属性表达式 7.4.4。特殊参数处理 7.4.5。限制查询结果 7.4.6。流式查询结果 7.4.7。异步查询结果 7.5。创建存储库实例 7.5.1。XML配置 7.5.2。JavaConfig 7.5.3。独立使用 7.6。Spring Data存储库的自定义实现 7.6.1。定制个人存储库 7.6.2。自定义基础存储库 7.7。从聚合根发布事件 7.8。Spring数据扩展 7.8.1。Querydsl扩展 7.8.2。Web支持 7.8.3。存储库填充程序 7.8.4。传统网络支持 参考文档 8.介绍 8.1。文档结构 9. MongoDB支持 9.1。入门 9.2。示例存储库 9.3。用Spring连接到MongoDB 9.3.1。使用基于Java的元数据注册Mongo实例 9.3.2。使用基于XML的元数据注册Mongo实例 9.3.3。MongoDbFactory接口 9.3.4。使用基于Java的元数据注册MongoDbFactory实例 9.3.5。使用基于XML的元数据注册MongoDbFactory实例 9.4。MongoTemplate简介 9.4.1。实例化MongoTemplate 9.4.2。WriteResultChecking策略 9.4.3。WriteConcern 9.4.4。WriteConcernResolver 9.5。保存,更新和删除文档 9.5.1。如何_id在映射图层中处理该字段 9.5.2。类型映射 9.5.3。保存和插入文件的方法 9.5.4。更新集合中的文档 9.5.5。在集合中插入文档 9.5.6。在集合中查找和插入文档 9.5.7。删除文件的方法 9.5.8。乐观锁定 9.6。查询文件 9.6.1。查询集合中的文档 9.6.2。查询文件的方法 9.6.3。查询不同的值 9.6.4。地理空间查询 9.6.5。GeoJSON支持 9.6.6。全文查询 9.6.7。排序规则 9.6.8。JSON模式 9.6.9。流利的模板API 9.7。按实例查询 9.7.1。介绍 9.7.2。用法 9.7.3。示例匹配器 9.7.4。执行一个例子 9.7.5。无类型示例 9.8。减少地图操作 9.8.1。使用示例 9.9。脚本操作 9.9.1。使用示例 9.10。集团运营 9.10.1。使用示例 9.11。聚合框架支持 9.11.1。基本概念 9.11.2。支持的聚合操作 9.11.3。投影表达式 9.11.4。分面分类 9.12。用自定义转换器覆盖默认映射 9.12.1。使用已注册的Spring Converter进行保存 9.12.2。使用Spring转换器读取 9.12.3。使用MongoConverter注册Spring转换器 9.12.4。转换器消除歧义 9.13。索引和集合管理 9.13.1。创建索引的方法 9.13.2。访问索引信息 9.13.3。使用集合的方法 9.14。执行命令 9.14.1。执行命令的方法 9.15。生命周期事件 9.16。例外翻译 9.17。执行回调 9.18。GridFS支持 9.19。更改流 9.19.1。使用MessageListener更改流 9.19.2。更改流 - 无效 10.反应性的MongoDB支持 10.1。入门 10.2。使用Spring和Reactive Streams Driver连接到MongoDB 10.2.1。使用基于Java的元数据注册MongoClient实例 10.2.2。ReactiveMongoDatabaseFactory接口 10.2.3。使用基于

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值