The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Quarkus でのソフトウェアトランザクションメモリーの使用

Software Transactional Memory (STM) has been around in research environments since the late 1990’s and has relatively recently started to appear in products and various programming languages. We won’t go into all the details behind STM but the interested reader could look at this paper. However, suffice it to say that STM offers an approach to developing transactional applications in a highly concurrent environment with some of the same characteristics of ACID transactions, which you’ve probably already used through JTA. Importantly though, the Durability property is relaxed (removed) within STM implementations, or at least made optional. This is not the situation with JTA, where state changes are made durable to a relational database which supports the X/Open XA standard.

Note that the STM implementation provided by Quarkus is based on the Narayana STM implementation. This document isn’t meant to be a replacement for that project’s documentation, so you may want to look at it for more detail. However, we will try to focus more on how you can combine some key capabilities into Quarkus when developing Kubernetes native applications and microservices.

なぜ Quarkus で STM を使うのか?

「なぜ JTA ではなく STM なのか?」あるいは「JTA では得られない STM のメリットとは?」と自問自答しているかもしれません。ここでは、STM が Quarkus、マイクロサービス、Kubernetes ネイティブアプリケーションに最適である理由を中心に、これらの質問や似たような質問に答えていきます。順序にはこだわりません。

  • The goal of STM is to simplify object reads and writes from multiple threads/protect state from concurrent updates. The Quarkus STM implementation will safely manage any conflicts between these threads using whatever isolation model has been chosen to protect that specific state instance (object in the case of Quarkus). In Quarkus STM, there are two isolation implementations, pessimistic (the default), which would cause conflicting threads to be blocked until the original has completed its updates (committed or aborted the transaction); then there’s the optimistic approach which allows all the threads to proceed and checks for conflicts at commit time, where one or more of the threads may be forced to abort if there have been conflicting updates.

  • STM objects have state, but it doesn’t need to be persistent (durable). In fact the default behaviour is for objects managed within transactional memory to be volatile, such that if the service or microservice within which they are being used crashes or is spawned elsewhere, e.g., by a scheduler, all state in memory is lost and the objects start from scratch. But surely you get this and more with JTA (and a suitable transactional datastore) and don’t need to worry about restarting your application? Not quite. There’s a trade-off here: we’re doing away with persistent state and the overhead of reading from and then writing (and sync-ing) to the datastore during each transaction. This makes updates to (volatile) state very fast, but you still get the benefits of atomic updates across multiple STM objects (e.g., objects your team wrote then calling objects you inherited from another team and requiring them to make all-or-nothing updates), as well as consistency and isolation in the presence of concurrent threads/users (common in distributed microservices architectures). Furthermore, not all stateful applications need to be durable - even when JTA transactions are used, it tends to be the exception and not the rule. And as you’ll see later, because applications can optionally start and control transactions, it’s possible to build microservices which can undo state changes and try alternative paths.

  • STM のもう 1 つの利点は、構成可能性とモジュール性です。オブジェクト/サービスの実装詳細を公開することなく、STM を使用して構築された他の任意のサービスと簡単に構成することができる Quarkus オブジェクト/サービスを並行して書くことができます。先ほど説明したように、他のチームと一緒に書いたオブジェクトを、数週間、数ヶ月、数年前に書いたかもしれない、A、C、I のプロパティーのあるオブジェクトをコンパイルできるこの機能は、非常に有益です。さらに、Quarkus が使用しているものを含むいくつかの STM 実装では、入れ子になったトランザクションをサポートしており、入れ子になった (サブ) トランザクションのコンテキスト内で行われた変更を、後で親トランザクションによってロールバックすることができます。

  • STMオ ブジェクトステートのデフォルトは揮発性ですが、オブジェクトの状態が耐久性を持つように STM の実装を構成することができます。リレーショナルデータベースなど、さまざまなバックエンドのデータストアを使用できるように Narayana を設定することは可能です。ただし、デフォルトはローカルのオペレーティングシステムのファイルシステムであり、データベースのように、Quarkus で他の何かを設定する必要はありません。

  • 多くの STM 実装では、アプリケーションコードをほとんど変更することなく、「古い言語オブジェクト」を STM 対応させることができます。アプリケーションを STM に対応させずに、アプリケーションの構築、テスト、デプロイし、必要になっときに、開発のオーバーヘッドがほとんどない状態で、これらの機能を後で追加することができます。

STM アプリケーションの構築

Quickstarts には完全に動作する例もあり、Git リポジトリーを複製してアクセスすることができます。git clone https://github.com/quarkusio/quarkus-quickstarts.git あるいは archive をダウンロードしてください。software-transactional-memory-quickstart の例を見てください。これで、Quarkus を使って STM を意識したアプリケーションを構築する方法を説明しています。しかし、その前に、いくつかの基本的な概念を知っておく必要があります。

Note, as you will see, STM in Quarkus relies on a number of annotations to define behaviours. The lack of these annotations causes sensible defaults to be assumed, but it is important for the developer to understand what these may be. Please refer to the Narayana STM manual and the STM annotations guide for more details on all the annotations Narayana STM provides.

この技術は、previewと考えられています。

preview では、下位互換性やエコシステムでの存在は保証されていません。具体的な改善には設定や API の変更が必要になるかもしれませんが、 stable になるための計画は現在進行中です。フィードバックは メーリングリストGitHub の課題管理 で受け付けています。

For a full list of possible statuses, check our FAQ entry.

設定

このエクステンションを使用するには、アプリケーションの依存関係として以下をビルドファイルにインクルードしてください:

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 の実装で定義されていました。特に Quarkus のような Kubernetes ネイティブ環境で使用すると混乱するかもしれませんが、読者の方には柔軟に理解していただければと思います。

The default STM container (org.jboss.stm.Container) provides support for volatile objects that can only be shared between threads in the same microservice/JVM instance. When an STM-aware object is placed into the container it returns a handle through which that object should then be used in the future. It is important to use this handle as continuing to access the object through the original reference will not allow the STM subsystem to track access and manage state and concurrency control.

    import org.jboss.stm.Container;

    ...

    Container<FlightService> container = new Container<>(); (1)
    FlightServiceImpl instance = new FlightServiceImpl(); (2)
    FlightService flightServiceProxy = container.create(instance); (3)
1 各コンテナーに、処理するオブジェクトの種類を伝える必要があります。この例では、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 と他の多くの便利なクラスがエクステンションに含まれています)。詳細は to the javadoc を参照してください。
2 プログラムでトランザクションを開始します。
3 また、オブジェクトの更新を構成することが可能です。つまり、複数のオブジェクトの更新を 1 つのアクションとしてまとめてコミットすることができます。[なお、ネストしたトランザクションを開始することで、推論的な作業を行うことも可能です。これは、その後、外側のトランザクションで行われた他の作業を放棄することなく、破棄できます。
4 Since the transaction has not yet been committed the changes made by the flight and taxi services are not visible outside the transaction.
5 コミットが成功したので、フライトサービスやタクシーサービスで行われた変更が他のスレッドからも見えるようになりました。古い状態に依存していた他のトランザクションがコミットする際に競合が発生する可能性があることに注意してください (STM ライブラリーは競合する動作を管理するための多くの機能を提供しており、これらについては Narayana STM マニュアルに記載されています)。
6 プログラム的には、フライトやタクシーサービスによって行われた変更が破棄されることを意味するトランザクションの中止を決定します。

分散型トランザクション

複数のサービス間でトランザクションを共有することは可能ですが、これは現在のところ高度なユースケースであり、この動作が必要な場合はNarayanaのドキュメントを参照してください。特に、STM は Context Propagation ガイドで説明されている機能をまだサポートしていません。