编辑此页面

在 Quarkus 中使用软件事务内存

自 1990 年代后期以来,软件事务内存 (STM) 一直存在于研究环境中,并且相对最近开始出现在产品和各种编程语言中。 我们不会详细介绍 STM 背后的所有细节,但感兴趣的读者可以查看这篇论文。 然而,可以肯定地说,STM 提供了一种在高度并发环境中开发事务性应用程序的方法,该方法具有 ACID 事务的一些相同特征,您可能已经通过 JTA 使用过它。 但重要的是,Durability(持久性)属性在 STM 实现中被放宽(删除),或者至少是可选的。 这与 JTA 的情况不同,在 JTA 中,状态更改会持久保存到支持 X/Open XA 标准的关系数据库中。

请注意,Quarkus 提供的 STM 实现基于 Narayana STM 实现。 本文档并非旨在替代该项目的文档,因此您可能需要查看它以获取更多详细信息。 但是,我们将尝试更多地关注如何在开发 Kubernetes 原生应用程序和微服务时将一些关键功能组合到 Quarkus 中。

为什么在 Quarkus 中使用 STM?

现在您可能仍在问自己“为什么是 STM 而不是 JTA?”或“我从 STM 中获得的 JTA 没有的好处是什么?” 让我们尝试回答这些或类似的问题,特别关注为什么我们认为它们非常适合 Quarkus、微服务和 Kubernetes 原生应用程序。 所以没有特定顺序……

  • STM 的目标是简化从多个线程读取和写入对象/保护状态免受并发更新。 Quarkus STM 实现将安全地管理这些线程之间的任何冲突,使用已选择的隔离模型来保护该特定状态实例(在 Quarkus 的情况下为对象)。 在 Quarkus STM 中,有两种隔离实现,悲观(默认),这将导致冲突的线程被阻塞,直到原始线程完成其更新(提交或中止事务); 然后是乐观方法,它允许所有线程继续进行,并在提交时检查冲突,如果存在冲突的更新,则可能会强制中止一个或多个线程。

  • STM 对象具有状态,但它不需要是持久的(耐用的)。 事实上,默认行为是事务内存中管理的对象是易失性的,因此如果使用它们的微服务或微服务崩溃或在其他地方生成(例如,由调度程序生成),则内存中的所有状态都会丢失,并且对象会从头开始。 但是,您当然可以通过 JTA(以及合适的事务性数据存储)获得更多,而无需担心重新启动您的应用程序? 不完全是。 这里有一个权衡:我们正在消除持久状态以及每次事务期间从数据存储读取然后写入(和同步)到数据存储的开销。 这使得对(易失性)状态的更新非常快,但您仍然可以获得跨多个 STM 对象的原子更新的好处(例如,您的团队编写的对象,然后调用您从另一个团队继承的对象,并要求它们进行全有或全无的更新),以及在并发线程/用户存在的情况下的一致性和隔离(在分布式微服务架构中很常见)。 此外,并非所有有状态应用程序都需要持久 - 即使使用 JTA 事务,它也往往是例外而不是规则。 正如您稍后将看到的,由于应用程序可以选择启动和控制事务,因此可以构建可以撤消状态更改并尝试替代路径的微服务。

  • STM 的另一个好处是可组合性和模块化。 您可以编写并发的 Quarkus 对象/服务,这些对象/服务可以轻松地与使用 STM 构建的任何其他服务组合,而无需公开对象/服务的实现细节。 正如我们之前讨论的那样,这种将您编写的对象与那些其他团队可能在数周、数月或数年前编写的对象组合的能力,并具有 A、C 和 I 属性,可能非常有利。 此外,一些 STM 实现(包括 Quarkus 使用的那个)支持嵌套事务,这些事务允许在嵌套(子)事务的上下文中进行的更改稍后被父事务回滚。

  • 虽然 STM 对象状态的默认设置是易失性的,但可以配置 STM 实现,以使对象的状态是持久的。 虽然可以配置 Narayana 以便可以使用不同的后端数据存储(包括关系数据库),但默认的是本地操作系统文件系统,这意味着您无需使用 Quarkus 配置任何其他内容,例如数据库。

  • 许多 STM 实现允许通过很少或不更改应用程序代码来使“普通旧语言对象”具有 STM 感知能力。 您可以构建、测试和部署应用程序,而无需它们具有 STM 感知能力,然后在必要时添加这些功能,而无需付出太多的开发开销。

构建 STM 应用程序

快速入门中还有一个完整的示例,您可以通过克隆 Git 存储库来访问该示例:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或者通过下载存档。 查找 software-transactional-memory-quickstart 示例。 这将有助于了解如何使用 Quarkus 构建具有 STM 感知能力的应用程序。 但是,在此之前,我们需要介绍一些基本概念。

请注意,正如您将看到的,Quarkus 中的 STM 依赖于许多注释来定义行为。 缺少这些注释会导致假定合理的默认值,但开发人员必须了解这些默认值。 有关 Narayana STM 提供的所有注释的更多详细信息,请参阅 Narayana STM 手册STM 注释指南

此技术被认为是预览版。

预览中,不保证向后兼容性和生态系统中存在。 具体改进可能需要更改配置或 API,并且正在制定成为稳定的计划。 欢迎在我们的邮件列表或我们GitHub 问题跟踪器中提供反馈。

有关可能的完整状态列表,请查看我们的常见问题解答条目

设置

要使用扩展,请将其作为依赖项包含在您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-narayana-stm</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-narayana-stm")

定义 STM 感知类

为了使 STM 子系统了解哪些类将在事务内存的上下文中进行管理,有必要提供最低级别的检测。 这是通过接口边界对 STM 感知类和 STM 非感知类进行分类来实现的; 具体来说,所有 STM 感知对象都必须是从本身已通过注释标识为 STM 感知对象的接口继承的类的实例。 任何不遵循此规则的其他对象(及其类)将不会由 STM 子系统管理,因此它们的任何状态更改都不会被回滚,例如。

STM 感知应用程序接口必须使用的特定注释是 org.jboss.stm.annotations.Transactional。 例如

@Transactional
public interface FlightService {
    int getNumberOfBookings();
    void makeBooking(String details);
}

实现此接口的类能够使用 Narayana 中的其他注释来告知 STM 子系统有关诸如方法是否会修改对象的状态,或者应该以事务方式管理类中的哪些状态变量之类的信息,例如,如果事务中止,则某些实例变量可能不需要回滚。 如前所述,如果不存在这些注释,则会选择默认值以保证安全,例如假设所有方法都会修改状态。

public class FlightServiceImpl implements FlightService {
    @ReadLock
    public int getNumberOfBookings() { ... }
    public void makeBooking(String details) {...}

    @NotState
    private int timesCalled;
}

例如,通过在 getNumberOfBookings 方法上使用 @ReadLock 注释,我们可以告知 STM 子系统,当它在事务内存中使用时,此对象中不会发生状态修改。 此外,@NotState 注释告诉系统在事务提交或中止时忽略 timesCalled,因此该值仅因应用程序代码而更改。

有关如何对实现带有 @Transactional 注释标记的接口的对象的事务行为进行更精细控制的详细信息,请参阅 Narayana 指南。

创建 STM 对象

需要告知 STM 子系统它应该管理哪些对象。 Quarkus(又名 Narayana)STM 实现通过提供事务内存容器来做到这一点,这些容器中驻留着这些对象实例。 在将对象放入这些 STM 容器之一之前,无法在事务中管理该对象,并且任何状态更改都不具有 A、C、I(甚至 D)属性。

请注意,“容器”一词是在 Linux 容器出现之前的 STM 实现中定义的。 在 Kubernetes 原生环境中(例如 Quarkus)使用它可能会造成混淆,但希望读者能够进行心理映射。

默认的 STM 容器 (org.jboss.stm.Container) 提供对易失性对象的支持,这些对象只能在同一微服务/JVM 实例中的线程之间共享。 当将 STM 感知对象放入容器中时,它会返回一个句柄,通过该句柄将来应使用该对象。 重要的是使用此句柄,因为继续通过原始引用访问对象将不允许 STM 子系统跟踪访问并管理状态和并发控制。

    import org.jboss.stm.Container;

    ...

    Container<FlightService> container = new Container<>(); (1)
    FlightServiceImpl instance = new FlightServiceImpl(); (2)
    FlightService flightServiceProxy = container.create(instance); (3)
1 您需要告知每个 Container 它将负责的对象类型。 在此示例中,它将是实现 FlightService 接口的实例。
2 然后,创建一个实现 FlightService 的实例。 在此阶段,您不应直接使用它,因为 STM 子系统未管理对它的访问。
3 要获取托管实例,请将原始对象传递到 STM container,然后它将返回一个引用,您可以通过该引用执行事务操作。 此引用可以从多个线程安全地使用。

定义事务边界

一旦将对象放入 STM 容器中,应用程序开发人员就可以管理事务的范围,在该范围中使用它。 有一些注释可以应用于 STM 感知类,以便在调用特定方法时使容器自动创建事务。

声明式方法

如果将 @NestedTopLevel@Nested 注释放置在方法签名上,则 STM 容器将在调用该方法时启动新事务,并在该方法返回时尝试提交该事务。 如果已经有一个事务与调用线程关联,则每个注释的行为略有不同:前一个注释将始终创建一个新的顶级事务,方法将在该事务中执行,因此封闭事务不会像父事务一样运行,即嵌套的顶级事务将独立提交或中止; 后一个注释将创建一个事务,该事务正确嵌套在调用事务中,即该事务充当此新创建的事务的父事务。

编程式方法

应用程序可以在访问 STM 对象的方法之前以编程方式启动事务

AtomicAction aa = new AtomicAction(); (1)

aa.begin(); (2)
{
    try {
        flightService.makeBooking("BA123 ...");
        taxiService.makeBooking("East Coast Taxis ..."); (3)
        (4)
        aa.commit();
        (5)
    } catch (Exception e) {
        aa.abort(); (6)
    }
}
1 一个用于手动控制事务边界的对象(扩展中包含 AtomicAction 和许多其他有用的类)。 有关更多详细信息,请参阅 javadoc
2 以编程方式开始事务。
3 请注意,可以组合对象更新,这意味着可以将对多个对象的更新作为单个操作一起提交。[请注意,也可以开始嵌套事务,以便您可以执行推测性工作,然后可以放弃该工作,而无需放弃外部事务执行的其他工作]。
4 由于事务尚未提交,因此航班和出租车服务所做的更改在事务外部不可见。
5 由于提交成功,航班和出租车服务所做的更改现在对其他线程可见。 请注意,当其他依赖于旧状态的事务提交时,现在可能会或可能不会发生冲突(STM 库提供了许多用于管理冲突行为的功能,这些功能已在 Narayana STM 手册中介绍)。
6 以编程方式决定中止事务,这意味着航班和出租车服务所做的更改将被丢弃。

分布式事务

可以在多个服务之间共享事务,但这目前仅是一种高级用例,如果需要此行为,应查阅 Narayana 文档。 特别是,STM 尚不支持 上下文传播指南中描述的功能。

相关内容