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

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

ソフトウェアトランザクショナルメモリー (STM) は、1990 年代後半から研究されており、最近になって製品やさまざまなプログラミング言語に登場し始めました。STM の背後にあるすべての詳細については触れませんが、興味のある方は この論文 を参照してください。ただし、STM は、おそらく JTA を通じて既に使用したことがあるACID トランザクションと同じ特性をいくつか備えた高度な並行環境でトランザクション アプリケーションを開発するためのアプローチを提供する、とだけ言っておけば十分でしょう。重要なのは、STM の実装では、Durability プロパティーが緩和 (削除) されているか、少なくともオプションになっていることです。これは、X/Open XA 標準 をサポートするリレーショナルデータベースに対して状態の変更を耐久性のあるものにするという JTA の状況とは異なります。

Quarkus が提供する STM の実装は、 Narayana STM の実装に基づいています。このドキュメントは、そのプロジェクトのドキュメントに代わるものではありません。そのため、その詳細についてはそちらを参照してください。しかし、Kubernetes ネイティブアプリケーションやマイクロサービスを開発する際に、主要な機能のいくつかを Quarkus にどのように組み合わせることができるかについて、より焦点を当ててみたいと思います。

なぜ Quarkus で STM を使うのか?

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

  • STM の目的は、オブジェクト読み込みを簡素化し、複数のスレッドから書き込みを行い、同時更新からの状態を保護することです。Quarkus の STM 実装では、特定のステートインスタンス (Quarkus の場合はオブジェクト) を保護するために選択された分離モデルを使用して、これらのスレッド間の競合を安全に管理します。Quarkus STM で は、2つの分離実装があります。ペシミスティックアプローチ (デフォルト) では、元のスレッドが更新を完了する (コミットまたはトランザクションを中止する) まで競合するスレッドがブロックされます。一方、オプティミスティックアプローチでは、すべてのスレッドの続行を許可し、同時に競合をチェックします。ここでは、競合更新がある場合は、1 つ以上のスレッドが強制的に中止されることがあります。

  • STM オブジェクトには状態 (state) がありますが、永続的 (durable) である必要はありません。実際、デフォルトの動作は、トランザクションメモリー内で管理されているオブジェクトが揮発性になるためのものです。使用されているサービスやマイクロサービスがクラッシュしたり、スケジューラーなどによって別の場所で生成されたりすると、メモリー内のすべてのステートが失われ、オブジェクトはゼロからスタートします。しかし、これは確かに発生する (JTA (と適切なトランザクションデータストア) では特に) ことですが、アプリケーションの再起動を心配する必要はないのでしょうか?そうではありません。ここではトレードオフがあります。永続的な状態と、各トランザクション中にデータストアから読み込み、そして書き込み (および同期) というオーバーヘッドを排除しています。これにより、(揮発性) ステートへの更新は非常に高速になりますが、複数の STM オブジェクト (例えば、自分のチームが書いたオブジェクトが、他のチームから継承したオブジェクトを呼び出して、all-or-nothing 方式の更新を行う必要がある場合など) にまたがるアトミックな更新の利点、同時スレッドユーザーの存在下 (分散マイクロサービスアーテクチャーでは一般的) で一貫性と分離を確立することができます。さらに、すべてのステートフルアプリケーションが耐久性を必要とするわけではありません。これは JTA トランザクションが使用される場合でも、それは例外であってルールではない傾向があります。後述するように、アプリケーションは任意でトランザクションを起動したり制御したりできるので、状態変化を元に戻したり、別のパスを試したりできるマイクロサービスを構築することが可能です。

  • 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 を意識したアプリケーションを構築する方法を説明しています。しかし、その前に、いくつかの基本的な概念を知っておく必要があります。

ご覧のように、Quarkus の STM は、動作を定義するために多くのアノテーションに依存しています。これらのアノテーションがないために、適切なデフォルトが仮定されますが、開発者はこれらのアノテーションが何であるかを理解することが重要です。Narayana STM が提供するすべてのアノテーションの詳細については、 Narayana STM manualSTM アノテーションガイド を参照してください。

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

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

とりうるステータスの完全なリストについては、 FAQの項目 を参照してください。

設定

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

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

デフォルトの 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 各コンテナーに、処理するオブジェクトの種類を伝える必要があります。この例では、FlightService インターフェースを実装したインスタンスになります。
2 次に、FlightService を実装したインスタンスを作成します。この段階では、STM サブシステムによってアクセスが管理されていないため、直接使用してはいけません。
3 管理されたインスタンスを取得するには、元のオブジェクトを STM container に渡します。すると、トランザクション的な操作を実行できるようになる参照を返します。この参照は、複数のスレッドから安全に使用することができます。

トランザクションの境界の定義

一度オブジェクトをSTMコンテナー内に配置すると、アプリケーション開発者はそれが使用されるトランザクションの範囲を管理することができます。特定のメソッドが呼び出されたときにコンテナーが自動的にトランザクションを作成するように、STM-aware クラスに適用できるアノテーションがいくつかあります。

宣言的アプローチ

@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 このトランザクションはまだコミットされていないため、フライトやタクシーサービスによって行われた変更は、トランザクションの外からは見えません。
5 コミットが成功したので、フライトサービスやタクシーサービスで行われた変更が他のスレッドからも見えるようになりました。古い状態に依存していた他のトランザクションがコミットする際に競合が発生する可能性があることに注意してください (STM ライブラリーは競合する動作を管理するための多くの機能を提供しており、これらについては Narayana STM マニュアルに記載されています)。
6 プログラム的には、フライトやタクシーサービスによって行われた変更が破棄されることを意味するトランザクションの中止を決定します。

分散型トランザクション

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

関連コンテンツ