事件溯源(Event Sourcing)和命令查询责任分离(CQRS)经验

img

这篇文章是实现一个基于 CQRS 和事件溯源原则的应用程序,描述这个过程的方式,我相信分享我面临的挑战和问题可能对一些人有用。特别是如果你正在开始自己的旅程。

业务背景

项目的背景与空中交通管理(ATM)领域相关。我们为一个 ANSP(航空导航服务提供商)设计了一个解决方案,负责控制特定地理区域。这个应用程序的目标很简单:计算并持久化飞行数据。流程大致如下。

在飞机穿越其领空之前的几个小时,ANSP会收到来自 Eurocontrol 的信息,这个组织负责管理整个欧洲的航空交通。这些信息包含计划数据,如飞机类型、起飞地点、目的地、请求的航路等。一旦飞机到达了 ANSP 的 AOR(责任区域,ANSP负责控制和监控航班的区域),我们就可以从各种来源接收输入:航迹更新(飞行当前位置是什么)、修改当前航路的请求、由轨迹预测系统触发的事件、来自冲突检测系统的警报等。

img

虽然我们可能需要同时处理多个潜在的并发请求,但在吞吐量方面,与 PayPal 或 Netflix 没法相提并论。

尽管如此,该应用程序是安全关键环境的一部分。在发生关键故障的情况下,我们不会损失金钱或客户,但我们可能会失去人的生命。因此,实现一个可靠、响应迅速且具有弹性的系统,以保证数据一致性和完整性,显然是首要任务。

CQRS、事件溯源

这两种模式实际上都相当容易理解。

CQRS

CQRS(命令查询责任分离)是一种将写入(命令)和读取(查询)分离的方式。这意味着我们可以有一个数据库来管理写入部分。而读取部分(也称为视图或投影)是从写入部分派生出来的,可以由一个或多个数据库来管理(取决于我们的用例)。大多数情况下,读取部分是异步计算的,这意味着两个部分并不严格一致。我们稍后会回到这一点。

CQRS 背后的想法之一是数据库很难同时高效地处理读写操作。这可能取决于软件供应商的选择、应用的数据库调优等。例如,Apache Cassandra 在持久化数据方面效率很高,而 Elasticsearch 对搜索非常出色。使用 CQRS 真的是利用解决方案的优势的一种方式。

此外,我们还可以决定处理不同的数据模型。再次强调,这取决于需求。例如,在报告视图上使用的模型,另一个在写入部分持久化期间效率高的非规范化模型等。

关于视图,我们可能决定实现一些与消费者无关的视图(例如公开特定的业务对象),或者一些专门针对消费者的视图。

事件溯源

根据 Martin Fowler 对事件溯源的定义:

确保应用状态的所有更改都存储为事件序列

这意味着我们不存储对象的状态。相反,我们存储影响其状态的所有事件。然后,要检索对象状态,我们必须读取与该对象相关的不同事件,并逐个应用它们。

CQRS + 事件溯源

这两种模式经常被组合在一起。在 CQRS 之上应用事件溯源意味着将每个事件持久化在我们应用程序的写入部分。然后,读取部分是从事件序列派生出来的。

在我看来,实现 CQRS 时并不需要事件溯源。然而,反之则不一定成立。

事实上,对于大多数用例,在实现事件溯源时需要 CQRS,因为我们可能希望以 O(1) 的时间复杂度检索状态,而不必计算 n 种不同的事件。一个例外是简单的审计日志用例。在这里,我们不需要管理视图(也不需要状态),因为我们只对检索日志序列感兴趣。

领域驱动设计

领域驱动设计(DDD)是一种处理与领域模型相关的软件复杂性的方法。它由 Eric Evans 在 2004 年的《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书中引入。

我们不会介绍所有不同的概念,但如果你对此不熟悉,我强烈建议你去看一看。不过,我们将只介绍在 CQRS/事件溯源应用程序环境中有用的概念。

DDD 带来的第一个概念是聚合(Aggregate)。聚合是一组领域对象,从数据变更角度来看,它们被视为一个单元。在聚合内部的事务必须保持原子性。

与此同时,聚合通过不变式来强制执行自己的数据一致性和完整性。不变式就是一个规则,无论如何变化,它都必须保持为真。例如,标准终端到达航线(STAR,基本上是着陆前的预定义航线)始终与给定机场相关联。一个不变式必须强制执行这样一个规则:目的机场不能在没有更改 STAR 的情况下被修改,并且这个 STAR 与该机场是有效的。

此外,作为聚合的外观对象(处理输入并将业务逻辑委托给子对象的对象)被称为聚合根。

关于组成聚合的对象,我们需要区分实体和值对象。实体是具有标识的对象,它不是由其属性定义的。一个人的年龄会随着时间的推移而改变,但他/她仍然是同一个人。另一方面,值对象仅由其属性定义。不同城市的地址是不同的地址。前者是可变的,而后者是不可变的。此外,实体可以有自己的生命周期。例如,一个航班首先准备起飞,然后是空中飞行,最后着陆。

在模型定义中,实体应尽可能简单,并专注于其标识和其生命周期。在 CQRS/事件溯源应用程序的上下文中,实体是一个关键元素,因为大多数情况下,在聚合内进行的更改是基于它们的生命周期。例如,至关重要的是确保每个实体实现了一个函数,用于确定它是否与另一个实体实例相等。这可以通过比较标识符或一组相关属性来实现,从而保证了一个标识。

既然我们已经了解了实体的概念,让我们回到不变式。为了定义它们,我们使用了受 BDD(行为驱动开发)格式启发的语言:

Given [entity] at state [state]
When [event] occurs
We shall [rules]

领域驱动设计(DDD)是一种处理与领域模型相关的软件复杂性的方法。它由 Eric Evans 在 2004 年的《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书中引入。

我们不会介绍所有不同的概念,但如果你对此不熟悉,我强烈建议你去看一看。不过,我们将只介绍在 CQRS/事件溯源应用程序环境中有用的概念。

DDD 带来的第一个概念是聚合(Aggregate)。聚合是一组领域对象,从数据变更角度来看,它们被视为一个单元。在聚合内部的事务必须保持原子性。

与此同时,聚合通过不变式来强制执行自己的数据一致性和完整性。不变式就是一个规则,无论如何变化,它都必须保持为真。例如,标准终端到达航线(STAR,基本上是着陆前的预定义航线)始终与给定机场相关联。一个不变式必须强制执行这样一个规则:目的机场不能在没有更改 STAR 的情况下被修改,并且这个 STAR 与该机场是有效的。

此外,作为聚合的外观对象(处理输入并将业务逻辑委托给子对象的对象)被称为聚合根。

关于组成聚合的对象,我们需要区分实体和值对象。实体是具有标识的对象,它不是由其属性定义的。一个人的年龄会随着时间的推移而改变,但他/她仍然是同一个人。另一方面,值对象仅由其属性定义。不同城市的地址是不同的地址。前者是可变的,而后者是不可变的。此外,实体可以有自己的生命周期。例如,一个航班首先准备起飞,然后是空中飞行,最后着陆。

在模型定义中,实体应尽可能简单,并专注于其标识和其生命周期。在 CQRS/事件溯源应用程序的上下文中,实体是一个关键元素,因为大多数情况下,在聚合内进行的更改是基于它们的生命周期。例如,至关重要的是确保每个实体实现了一个函数,用于确定它是否与另一个实体实例相等。这可以通过比较标识符或一组相关属性来实现,从而保证了一个标识。

既然我们已经了解了实体的概念,让我们回到不变式。为了定义它们,我们使用了受 BDD(行为驱动开发)格式启发的语言:

img

应用程序设计

简而言之,应用程序接收命令并发布内部事件。这些事件被持久化到事件存储中,并发布给处理程序,这些处理程序负责更新视图。我们还可以决定在视图之上实现一个服务层(称为读处理程序)。

现在,让我们详细看看不同的场景。

聚合创建

命令处理程序接收一个 CreateFlight 命令,并在领域存储库中检查实例是否存在。这个领域存储库管理聚合实例。它首先在缓存中进行检查,如果对象不存在,则会在事件存储中进行检查。事件存储是一个用于持久化事件序列的数据库。我会稍后详细说明我认为一个好的事件存储是什么。在这种情况下,事件存储仍然为空,因此存储库不会返回任何内容。

命令处理程序负责触发不变式。在出现失败的情况下,我们可以同步抛出异常来指示业务问题。否则,命令处理程序将发布一个或多个事件到事件总线。事件的数量取决于内部数据模型的粒度。在我们的场景中,我们假设发布了一个单一的 FlightCreated 事件。

在此事件上触发的第一个组件是领域处理程序。这个组件负责根据实现的逻辑更新领域聚合。通常,逻辑被委托给聚合根(充当外观,但也可以将底层逻辑委托给子域对象)。请记住,聚合必须始终保持一致,并且还必须通过验证不变式来强制执行数据完整性。

如果处理程序成功(未引发业务错误),则事件将被持久化到事件存储中,并且缓存将使用最新的聚合实例进行更新。

然后,触发视图处理程序来更新其对应的视图。就像在普通的发布-订阅模式中一样,视图可以只订阅它感兴趣的事件。也许在我们的情况下,视图 2 是唯一对 FlightCreated 事件感兴趣的视图。

聚合更新

第二种情景是更新现有的聚合。在接收到 UpdateFlight 命令时,命令处理程序会请求存储库返回最新的聚合实例(如果有的话)。

如果实例已经在缓存中,则无需与事件存储交互。否则,存储库将触发所谓的重新装载过程。

这个过程是根据存储的事件序列计算聚合实例的当前状态的一种方式。从事件存储中检索的每个事件(比如 FlightCreatedDepartureUpdatedArrivalUpdated)都会被发布到事件总线。第一个领域处理程序触发 FlightCreated 时会实例化一个新的聚合(根据事件本身提供的信息,在内存中创建一个新的对象实例)。然后其他领域处理程序(由 DepartureUpdatedArrivalUpdated 事件触发)将更新刚刚创建的聚合实例。最终,我们能够根据存储的事件计算出状态。

一旦计算出状态,对象实例就会被放入缓存并返回给命令处理程序。然后,其余的流程与聚合创建情景相同。

关于重新装载过程还有一件事需要补充。如果一个聚合不在缓存中,而我们为一个特定的聚合实例存储了 1000 个事件,那么会花费很长时间来计算其状态。有一个已知的缓解措施叫做快照。

我们可以决定在每 n 个事件中持久化聚合的当前状态作为一个快照。这个快照也会包含在事件存储中的位置。然后,重新装载过程将简单地从最新的快照开始,并从指定的位置继续。快照还可以根据其他策略类型创建(如果重新装载时间超过某个阈值等)。

如何处理事件?

我想再回顾一下我们对命令和事件的区分。首先,有必要区分内部事件和外部事件。外部事件是由另一个应用程序产生的,而内部事件是由我们的应用程序生成的(基于外部命令)。

我们就如何在我们的应用程序中技术性地处理外部事件进行了一场有趣的辩论。我的意思是,真正的事件指的是已经在过去发生的事情(比如雷达轨迹)。

实际上有两种可能的处理方法:

  • 第一种方法是将事件视为命令。这意味着我们必须首先通过一个命令处理程序,验证不变式,然后生成一个内部事件。
  • 第二种方法是绕过命令处理程序,直接将事件持久化到事件存储中。毕竟,如果我们谈论的是一个真实事件,那么验证不变式等操作实际上是没有什么用的。然而,检查事件的语法仍然很重要,以确保我们不会污染事件存储。

如果我们选择第二个选项,可能会有兴趣在聚合重新装载期间实现规则。

让我们举一个雷达轨迹发布飞行位置的例子。如果生产者无法保证消息的顺序,我们还可以持久化一个时间戳(由生产者生成),并以这种方式计算状态:

if event.date > latestEventDate {  // Compute the statelatestEventDate = event.date} else {  // Discard the event}

这个规则将确保状态仅基于最新生成的事件。这意味着持久化一个事件不一定会影响当前状态。

在第一种方法中,在持久化事件之前会实现这样的规则。

事件模型

在事件存储中持久化的事件是否需要创建一个统一的模型?在我看来,答案是否定的(至少大部分情况下是)。

首先,因为我们可能希望随着时间推移持久化不同的模型版本。在这种情况下,我们必须实现一种策略,将一个模型版本的事件映射到另一个模型版本。

我想用一个具体的例子来说明另一个好处。假设一个应用程序接收来自系统 A 和系统 B 的事件。这两个系统基于各自的数据模型发布飞行事件。如果我们创建一个通用数据模型 C,我们需要在持久化事件之前将 A 转换为 C 和 B 转换为 C。然而,在项目的某个阶段,我们只对来自 A 和 B 的某些信息感兴趣。这意味着 C 只是 A 和 B 的一个子集。

但是如果以后我们需要对应用程序进行一些改进,并管理来自 A 和 B 的额外元素怎么办?因为事件是使用 C 格式持久化的,所以这些元素就会被简单地丢失。另一方面,如果我们决定持久化 A 和 B 格式,我们可以简单地对命令处理程序进行一些改进,以管理这些元素。

最终一致性

理论

最终一致性是由 CQRS(大多数情况下)引入的一个概念。理解其影响和后果非常重要。

首先,值得一提的是有不同的一致性级别。

最终一致性是一个模型,我们可以确保数据会被复制(从 CQRS 应用程序的写入部分到读取部分)。问题在于我们无法确切保证何时复制完成。这会受到各种因素的影响,比如整体吞吐量、网络延迟等。这是最弱的一致性形式,但提供了最低的延迟。

在 CQRS 应用程序中应用最终一致性意味着在某个时刻,写入部分可能与读取部分不同步。

相反地,我们可以找到强一致性模型。除非我们在分布式系统中使用相同的数据库来管理读取和写入,或者我们通过使用两阶段提交向恶魔出卖了我们的灵魂,否则在分布式系统中我们不应该达到这种一致性级别。

最接近的实现方法是,如果我们有两个不同的数据库,那就在单个线程中管理所有操作。这个线程将负责将数据持久化到写入数据库和读取数据库(们)。一个线程还可以专门用于单个聚合实例,并按顺序处理传入的命令。然而,如果在同步视图时发生瞬态错误,会有什么影响?我们需要补偿其他视图和 CQRS 应用程序的写入部分吗?我们需要实现错误重试循环吗?我们需要通过暂停命令处理程序来停止新的传入事件,应用断路器模式吗?解决显然会发生的瞬态错误是很重要的(凡是可能出错的地方迟早会出错)。

在最终一致性和强一致性两种一致性模型之间,我们可以找到许多不同的模型:因果一致性、顺序一致性等。举例来说,客户端单调一致性模型仅在会话(应用程序或服务实例)内保证强一致性。因此,实现 CQRS 应用程序并不只是在最终一致性和强一致性之间做出选择。

我个人的观点是:由于我们几乎无法保证强一致性,让我们尽可能地接受最终一致性。然而,前提是要精确理解其对系统其余部分的影响。

例子

让我们看一个我在项目中遇到的具体例子。

其中一个挑战是管理每架飞机的唯一标识符。我们不得不处理来自外部系统(公司外部)的事件,这些系统中的标识符并不相同。对于一个通道,标识符是一个复合标识符(出发机场 + 出发时间 + 飞机标识符 + 到达机场),而另一个通道则发送每架飞机的唯一标识符(但第一个通道不知道)。我们的目标是管理我们自己的唯一标识符(称为 GUFI,即全局唯一飞行标识符),并确保每个事件都对应于正确的 GUFI。

最简单的解决方案是确保每个传入的事件都在我们应用程序的特定视图中进行查找,以关联相应的 GUFI。但如果这个视图是最终一致的呢?在最坏的情况下,我们可能会有与同一飞行相关的事件,但使用不同的 GUFIs 进行存储(相信我,这是一个问题)。

一个解决方案可能是将这个 GUFI 的管理委托给另一个强一致性的服务。

在一次问答环节中,Greg Young 提供了另一个解决方案。我们可以实现一种缓冲区,其中只包含我们应用程序处理的 n 个最新事件。如果视图中不包含我们正在寻找的数据,我们必须在这个缓冲区中检查,以确保它不是刚刚在视图之前接收到的。n 越大,减轻写入和读取之间的这种不一致性窗口的机会就越大。

这个缓冲区可以使用像 Hazelcast、Redis 等解决方案进行分布式处理,也可以局部于应用程序实例。在后一种情况下,我们可能需要实现一个分片机制,使用哈希函数将相关对象的事件始终分发到相同的应用程序实例(最好是使用一种一致性哈希函数,以便轻松扩展)。

并发管理

几个月前我已经创建了一篇文章,描述了使用事件源管理并发更新的好处。

简而言之,拥有事件存储可能会帮助我们找到比悲观或乐观方法更聪明的解决方案来处理并发更新。

此外,在数据模型中应用正确的粒度也是项目成功的关键。

选择事件存储

我们可以决定使用任何类型的数据库来持久化事件序列。然而,最优解往往是为事件源构建的解决方案。

例如,隔离一个聚合实例是必须考虑的事情。假设所有事件都存储在一个单一表中。这个表会随着时间不断增长,在聚合重建时,我们将不得不过滤与一个特定聚合实例相关的事件。重建一个聚合的时间将取决于持久化的事件总数,即使其中一些事件与我们感兴趣的实例无关。一个好的解决方案可能是为每个聚合实例拥有一个表/存储桶,以隔离事件。我们称这个概念为流(stream)。一个流总是与一个聚合实例相关联(在大多数用例中)。

以下是我们考虑选择事件存储时的要求:

写入:

  • 恒定的写入延迟:无论流的大小如何,持久化事件的延迟都必须保持恒定
  • 原子性:可以在单个事务中追加多个事件
  • TTL 管理:根据创建日期自动丢弃事件
  • 无模式:可以存储多种事件类型和版本

读取:

  • 按写入顺序读取事件
  • 从特定序列号读取(因为快照)
  • 在给定流中保持恒定的读取性能,不受其他流的影响
  • 图形用户界面(GUI)
  • 缓存管理

并发:

  • 乐观并发模型
  • 幂等性管理

产品监控

解决方案支持

安全性:

  • 加密(传输)
  • 身份验证
  • 授权管理

扩展性

备份

每个上下文都是独特的,我相信你会有自己的要求,但这至少可能是一个起点。

结论

CQRS 和事件源并非魔法。在开始你的旅程之前,理解这两种模式的许多影响至关重要。否则,在技术和功能层面都很容易造成彻底的混乱。

然而,一旦你对约束和缺点有了明确的理解,CQRS 和/或事件源可能是许多问题的很好解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/200278.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MATLAB | 绘图复刻(十三) | 带NaN图例的地图绘制

有粉丝问我地图绘制如何添加NaN,大概像这样: 或者这样: 直接上干货: 原始绘图 假设我们有这样的一张图地图,注意运行本文代码需要去matlab官网下载Mapping Toolbox工具箱,但是其实原理都是相似的&…

level=warning msg=“failed to retrieve runc version: signal: segmentation fault“

安装docker启动后,发现里面没有runc版本信息 目前看是少了runc组件 那我们安装runc https://github.com/opencontainers/runc/releases/download/v1.1.10/runc.amd64 [rootlocalhost ~]# mv runc.amd64 /usr/bin/runc mv:是否覆盖"/usr/bin/runc&q…

【算法设计实验三】动态规划解决01背包问题

请勿原模原样复制! 01背包dp具体解释详见链接 ↓ 【算法5.1】背包问题 - 01背包 (至多最大价值、至少最小价值)_背包问题求最小价值_Roye_ack的博客-CSDN博客 关于如何求出最优物品选择方案? 先在递归求dp公式时,若…

Windows 安装 Docker

目录 前言安装 WSL2WSL2 简介系统要求安装步骤 安装 Docker Desktop下载安装验证 安装 Docker Compose结语开源项目 前言 下图展示了在 Windows 系统上安装 Docker,并利用Docker Compose一键搭建 youlai-mall 微服务商城所需的环境。本篇将先介绍 Windows 上如何安…

linux 系统调用流程分析

x86 1.系统调用 系统调用是用户空间程序与内核交互的主要机制。系统调用与普通函数调用不同,因为它调用的是内核里的代码。使用系统调用时,需要特殊指令以使处理器权限转换到内核态。另外,被调用的内核代码由系统调用号来标识,而…

树与二叉树堆:堆

堆的概念: 一般是把数组的数据在逻辑结构上看成一颗完全二叉树,如下图所示。 注意:别将C语言中的堆和数据结构的堆混为一谈,本文所讲的数据结构的堆是一种完全二叉树,而C语言中的堆其实是一种内存区域的划分 堆的分类…

VMware——WindowServer2012R2环境安装mysql5.7.14解压版_互为主从(图解版)

目录 一、服务器信息二、192.168.132.35服务器上安装mysql(主)2.1、环境变量配置2.2、安装2.2.1、修改配置文件内容2.2.2、初始化mysql并指定超级用户密码2.2.3、安装mysql服务2.2.4、启动mysql服务2.2.5、登录用户管理及密码修改2.2.6、开启远程访问 三…

docker更换国内源

docker更换国内源 1、编辑Docker配置文件 在终端中执行以下命令,编辑Docker配置文件: vi /etc/docker/daemon.json2、添加更新源 在打开的配置文件中,添加以下内容: {"registry-mirrors": ["https://hub-mirror…

gitlab环境准备

1.准备环境 gitlab只支持linux系统,本人在虚拟机下使用Ubuntu作为操作系统,gitlab镜像要使用和操作系统版本对应的版本,(ubuntu18.04,gitlab-ce_13.2.3-ce.0_amd64 .deb) book100ask:/$ lsb_release -a No LSB modules are available. Dist…

03-瑞吉外卖关于菜品/套餐分类表的增删改查

新增菜品/套餐分类 页面原型 当我们在后台系统中添加菜品/套餐时,需要选择一个菜品/套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐 第一步: 用户点击确定按钮执行submitForm函数发送Ajax请求,将新增菜品/套餐表单中输入的数据以json形式提交给服务端,…

算法分析与设计课后练习22

设W(5,7,10,12,15,18,20)和M35,使用过程SUMOFSUB找出W种使得和数等于M的全部子集并画出所生成的部分状态空间树

CV计算机视觉每日开源代码Paper with code速览-2023.11.16

点击CV计算机视觉,关注更多CV干货 论文已打包,点击进入—>下载界面 点击加入—>CV计算机视觉交流群 1.【基础网络架构】ConvNet vs Transformer, Supervised vs CLIP: Beyond ImageNet Accuracy 论文地址:https://arxiv.org//pdf/23…

数据分析思维与模型:多维度拆解分析法

多维度拆解分析法"(Multi-Dimensional Analysis and Decomposition Method)是一种用于深入分析和解决复杂问题的方法论。这种方法侧重于从多个角度或维度来考察问题,以便于更全面地理解和解决它们。它通常包括以下几个步骤: …

环保回收信息展示预约小程序的效果如何

人们每天在线上的时间非常多,他们会通过线上寻找信息,而环保回收企业也在通过线上寻找客户,但受限于平台限制,无论引流获客还是营销互动、或是数据分析及全面管理方面都面对难题,其中微信/百度/快手/抖音/支付宝/快手等…

我在CSDN开组会1-蒙特卡洛模拟在矿床学的应用展望

各位老师、同学们,大家好。今天组会的内容是蒙特卡洛模拟在矿床学的应用展望。 为什么要讲蒙特卡洛模拟呢,因为我发现在地质学方面已经有不少应用,但是蒙特卡洛模拟延伸的知识太晦涩了,劝退了很多探究者们。因此,计划…

DSP介绍及CCS

文章目录 CCS版本编译器CCS使用注意严禁中文 CCS的基本操作新建工程导入现有工程调整字体的大小工程界面恢复标签的使用 仿真盒小虫子进入在线Debug 芯片TMS320F28355基本介绍特性 DSP中特殊指令dsp指令中的EALLOW EDIS CCS TI官网 版本 CCS版本: CCS8.3.1.0004_…

腾讯云HAI域AI作画

目录 🐳前言: 🚀了解高性能应用服务 HAI 👻即插即用 轻松上手 👻横向对比 青出于蓝 🐤应用场景-AI作画 🐤应用场景-AI对话 🐤应用场景-算法研发 🚀使用HAI进行…

Web 自动化神器 TestCafe—页面基本操作篇

前 言 Testcafe是基于node.js的框架,以操作简洁著称,是web自动化的神器 今天主要给大家介绍一下testcafe这个框架和页面元素交互的方法。 一、互动要求 使用 TestCafe 与元素进行交互操作,元素需满足以下条件:☟ 元素在 body 页…

怎么让NetCore接口支持Json参数

项目:NetCore Web API 接口支持Json参数需要安装Newtonsoft.Json.Linq和Microsoft.AspNetCore.Mvc.NewtonsoftJson Program代码 //支持json需要安装Microsoft.AspNetCore.Mvc.NewtonsoftJson using Newtonsoft.Json.Serialization;var builder WebApplication.Cr…

【Seata源码学习 】篇三 TM开启全局事务的过程

【Seata源码学习 】篇三 TM开启全局事务的过程 TM发送 单个或批量 消息 以发送GlobalBeginRequest消息为例 TM在执行拦截器链路前将向TC发送GlobalBeginRequest 消息 io.seata.tm.api.DefaultGlobalTransaction#begin(int, java.lang.String) Overridepublic String begin(…