第11章 一般的なトランザクションの問題

11.1. JBoss Transaction Serviceのトランザクションに関する高度な問題

アプリケーションプログラマ、クラス開発者両方がトランザクションを利用します。オペレーション全体あるいは一部をトランザクショナルにすることができます。本章は一般的にトランザクションを使うことで起こる問題で若干扱いにくいもの、またJBoss Transaction Service の詳細について説明しています。

11.1.1. トランザクションの確認

マルチスレッドのアプリケーションでは、トランザクションが有効な間、複数のスレッドが1つのトランザクションに紐付けされて、同じコンテキストを共有します。また、1つのスレッドがトランザクションを終了した場合、他のスレッドはそのトランザクション内で有効な状態を保ちます。分散環境であれば、トランザクションが終了するとトランザクションを必要とするスレッドがないことを確認するのが困難です。JBoss Transaction Service はデフォルトで、あるスレッドがトランザクションを終了しようとすると、別のスレッドがそのトランザクションを利用している場合は警告を発行します。ただし、トランザクション自体は終了することができます。その他に考えられる動作として、別のスレッドがすべてトランザクションコンテキストとの紐付けを解除するまでスレッドを終了することができないようになっています。そのため、JBoss Transaction Service は、com.arjuna.ats.arjuna.coordinator.CheckedAction クラスを提供し、スレッド/トランザクションの終了ポリシーをオーバーライドできるようになっています。各トランザクションはクラスインスタンスが1つ紐付けられており、トランザクションごとに独自の実装を提供することができます。
public class CheckedAction
{
    public CheckedAction ();

    public synchronized void check (boolean isCommit, Uid actUid,
				    BasicList list);
};
スレッドがトランザクションを終了しようしたにも拘らず、有効なスレッドがある場合、システムはそのトランザクションのCheckedAction オブジェクトでcheck メソッドを呼び出します。check メソッドに対するパラメータは以下の通りです。
isCommit
トランザクションがコミットの処理中か、ロールバックの処理中かを示します。
actUid
トランザクションの識別子
list
トランザクション内で現在有効なスレッドの全一覧
check メソッドを返すとトランザクションが終了されます。トランザクションのステートがcheck メソッドが呼び出されたときの状態から変わっている可能性があります。

11.1.2. 統計の収集

デフォルトでは、JBoss Transaction Service はトランザクションの履歴情報を保持しません。com.arjuna.ats.arjuna.coordinator.enableStatisticsのプロパティ変数をYESに設定することで履歴追跡を作動させることができます。履歴情報には、作成したトランザクション数、トランザクションの結果が含まれます。例11.1「トランザクションの統計」にある通り、com.arjuna.TxCore.Atomic.TxStats クラスを使い、トランザクショナルアプリケーションの実行中にこの情報をリクエストすることができます。

例11.1 トランザクションの統計

public class TxStats
{

    // Returns the number of transactions (top-level and nested)
    // created so far.

    public static int numberOfTransactions ();

    // Returns the number of nested (sub) transactions created so far.

    public static int numberOfNestedTransactions ();

    // Returns the number of transactions which have terminated with
    // heuristic outcomes.

    public static int numberOfHeuristics ();

    // Returns the number of committed transactions.

    public static int numberOfCommittedTransactions ();

    // Returns the number of transactions which have rolled back.

    public static int numberOfAbortedTransactions ();

}

11.1.3. 最終リソースコミットの最適化 (LRCO:Last Resource Commit Optimization)

場合によってはtwo-phase commit aware (2相コミット対応) でないパーティシパントを2相コミットトランザクションに登録する必要がある場合もあります。単一リソースのみであれば、2相コミットの必要はありません。トランザクションに複数のトランザクションが含まれている場合、Last Resource Commit Optimization (LRCO) が該当してきます。1相対応の単一リソースであれば、準備フェースなしでコミットあるいはロールバックのみとなっています。このようなリソースが2相コミット対応リソースを持つトランザクションに登録されている場合もあります。コーディネータは、他の全リソースに対して準備フェーズを先に実行することで、1相対応リソースと若干違った形式で処理をします。その後、1相対応のトランザクションは、そのトランザクションをコミットする必要があるため、コーディネータがトランザクションに制御を渡します。コミットすると、コーディネータはコミットが決定されたことをログに残し、他のリソースのコミットも試行します。
LRCO を利用するには、利用中のパーティシパントがcom.arjuna.ats.arjuna.coordinator.OnePhase を実装し、BasicAction.add メソッドでトランザクションに登録する必要があります。例11.2「BasicAction.add の例」にて説明されているように、このオペレーションでは、AbstractRecordのインスタンスを期待するため、com.arjuna.ats.arjuna.LastResourceRecord クラスのインスタンスを作成し、コンストラクタパラメータとしてパーティシパントに渡す必要があります。

例11.2 BasicAction.add の例

try
    {
	boolean success = false;
	AtomicAction A = new AtomicAction();
	OnePhase opRes = new OnePhase();  // used OnePhase interface
      
	System.err.println("Starting top-level action.");
      
	A.begin();
	A.add(new LastResourceRecord(opRes));
	A.add(new ShutdownRecord(ShutdownRecord.FAIL_IN_PREPARE));
      
	A.commit();
    }

11.1.4. ネスト化されたトランザクション

トランザクションをネストするために特別なことをする必要はありません。アクションが実行されているときに別のアクションが開始されると、自動にネスト化されます。こうすることで、アプリケーションにモジュラー構造を提供するため、オブジェクトのプログラマはオブジェクトを利用するアプリケーションもトランザクショナルかどうかに関して心配する必要がありません。
ネスト化されたアクションが中断されると、その作業がすべてロールバックされます。しかし、厳密な2相ロッキングでは、終了トランザクションが持つロックがトップレベルのアクションがコミットあるいは中断されるまで保持されるのです。ネスト化されたアクションがコミットされ、トップレベルのアクションがコミットされると、システムにより実行された作業のみがコミットされます。トップレベルのアクションが中断されると、作業はすべてロールバックされます。
ネスト化されたアクションをコミットあるいは中断した場合、親アクションの結果も自動的に影響を受けるわけではありません。この動作をプログラム的に実装するように選択し、障害を阻止する方法あるいは作業をやり直す方法などを制御することができます。

11.1.5. トランザクションを非同期的にコミット

デフォルトでは、JBoss Transaction Service は、1つのスレッドで同期するような形でトップレベルトランザクションのcommit プロトコルを実行します。1つのスレッドが登録済みの全リソースを順番に準備するために移動し、コミットあるいはロールバックを行います。これには、考えられるデメリットが何点かあります。
  • 通常、prepare の操作は各リソースごとに並行して論理的に呼び出されます。デメリットですが、登録されているリソースにあるリストの前のほうに出てくるリソースが強制的にprepareの段階でロールバックされてしまい、必要のないprepare 操作が実行されてしまうことがあります。
  • 利用中のアプリケーションがヒューリスティックなレポーティングを必要とする場合、成功、失敗は特に重要でないため、commit プロトコルの2番目のフェーズを非同期的に呼び出すことができます。
そのため、JBoss Transaction Service は特定の変数を設定することで、ランタイムオプションを提供し、スレッドの最適化を2つ有効にします。
com.arjuna.ats.arjuna.coordinator.asyncPrepare
YESに設定されている場合、prepareフェーズにてトランザクション内に登録パーティシパントごとに別のスレッドが作成されます。
com.arjuna.ats.arjuna.coordinator.asyncCommit
YESに設定されていると、ヒューリスティックな結果の情報を必要としない場合、別のスレッドが作成されトランザクションの2番目のフェーズを完了します。

11.1.6. 独立したトップレベルトランザクション

通常のネスト化されたトップレベルトランザクションに加え、JBoss Transaction Service は独立したトップレベルアクションをサポートし、厳密な直列化可能性を制御しながら緩和できます。もう1つのトランザクション内であればどこからでも独立したトップレベルアクションを実行可能で、通常のトップレベルアクションと全く同じように動作します。親アクションが中断された場合にやり直さず、コミットされた場合、その結果は永続化されます。
独立したトップレベルアクション

図11.1 独立したトップレベルアクション

図11.1「独立したトップレベルアクション」 では、アクションBがアクションAの中にネスト化されているといった、一般的なトランザクションのネスト化について説明しています。Bが有効な間にBegin 操作が呼び出されるため、トランザクションCが論理的にアクションB内にネスト化されます。しかし、独立したトップレベルアクションであるため、構造内にある別のアクションから独立してcommitあるいはabortが行われます。独立したトップレベルアクションにこのような特徴があるため、慎重にテストや所見を行った後にのみ、注意して利用するようにしてください。
TopLevelTransaction クラスのインスタンスを宣言し、他のトランザクションと全く同じ方法で利用することでアプリケーション内でトップレベルアクションを使うことができます。

11.1.7. save_staterestore_state メソッド内のトランザクション

save_staterestore_state メソッドを記述する際には注意が必要です。メソッド内で明示的に、あるいは他の操作を利用して暗黙的にアトミックなアクションを開始しないようにしてください。注意が必要な理由は、JBoss Transaction Service はコミットするとrestore_state メソッドを呼び出す可能性があり、別アクションのcommit あるいは abort フェーズ中にトランザクション実行を試行してしまいます。こうすることで、コミットあるいは中断されたアクションのアトミックなプロパティを妨害する可能性があるため、アトミックアクションの開始はお薦めできません。

11.1.8. 例

例11.3「ネスト化されたトランザクションの例」 にあるコードは前述した例10.7「オブジェクトのステートを保存、復元」 の例に基づいており、setgetメソッドを実装します。このコードは単純化されており、エラーの条件や例外は無視しています。

例11.3 ネスト化されたトランザクションの例

public boolean set (int index, int value)
{
    boolean result = false;
    AtomicAction A = new AtomicAction();

    A.begin();

    // We need to set a WRITE lock as we want to modify the state.

    if (setlock(new Lock(LockMode.WRITE), 0) == LockResult.GRANTED)
	{
	    elements[index] = value;
	    if ((value > 0) && (index > highestIndex))
		highestIndex = index;
	    A.commit(true);
	    result = true;
	}
    else
	A.rollback();

    return result;
}

public int get (int index)  // assume -1 means error
{
    AtomicAction A = new AtomicAction();

    A.begin();

    // We only need a READ lock as the state is unchanged.

    if (setlock(new Lock(LockMode.READ), 0) == LockResult.GRANTED)
	{
	    A.commit(true);

	    return elements[index];
	}
    else
	A.rollback();

    return -1;
}

11.1.9. ガーベッジコレクションのオブジェクト

Java オブジェクトは必要なくなると、ガーベッジコレクタにより削除されます。トランザクションの制御下にあるオブジェクトを削除する場合は注意が必要です。オブジェクトがトランザクション内で操作されている場合、トランザクションがオブジェクトに何が起こるか制御します。そのため、アプリケーションが保持するトランザクショナルオブジェクトに対する参照に関係なく、JBoss Transaction Service は常に独自の参照を保持しており、関連するトランザクションがすべて終了されるまで、オブジェクトがガーベッジコレクタにより削除されないようにします。

11.1.10. トランザクションのタイムアウト

デフォルトでは、トランザクションを作成したアプリケーションがトランザクションを削除するまで、あるいは障害が発生するまで、トランザクションは有効です。しかし、トランザクションごとにタイムアウトを設定することができ、タイムアウト失効前に終了しないトランザクションがロールバックされるようになります。タイムアウトの値は秒数で表現します。
JBoss Transaction Service では、タイムアウト値をAtomicAction コンストラクタへのパラメータとして渡します。デフォルト値AtomicAction.NO_TIMEOUTとなっている場合、トランザクションは自動でタイムアウトされません。その他の正の値であれば、トランザクションが終了されるまで待機する秒数を示します。0の値は、グローバルのデフォルトタイムアウトとして解釈され、プロパティcom.arjuna.ats.arjuna.coordinator.defaultTimeoutを使い提供できます。このプロパティのデフォルト値は、60あるいは1分となっています。
タイムアウトが0以外のトップレベルトランザクションを作成すると、トランザクションのタイムアウト時にロールバックされます。JBossTS は別の reaper スレッドを使い、ローカルで作成されたトランザクションをすべて監視することで、タイムアウトが過ぎると強制的にトランザクションをロールバックします。Reaper スレッドがアプリケーションの時間を消費しないように一定期間あけて実行されるようになっています。デフォルトのチェック期間は12万ミリ秒ですが、com.arjuna.ats.arjuna.coordinator.txReaperTimeout プロパティを別の値 (ミリ秒) に設定することで値をオーバーライドすることができます。また、com.arjuna.ats.arjuna.coordinator.txReaperModeプロパティをDYNAMICに設定すると、トランザクションがタイムアウトすると、トランザクション reaper が起動します。これにより、トランザクションを早めに終了できるという利点がありますが、継続的にReaper スレッドを再スケジュールしてしまう可能性があります。
トップレベルトランザクションに対するタイムアウトの値が0 の場合、あるいはタイムアウトの値が指定されていない場合、JBoss Transaction Service はそのトランザクションに対してタイムアウトを行わず、永遠に実行しつづけることが可能になります。com.arjuna.ats.arjuna.coordinator.defaultTimeoutプロパティを設定することで、このデフォルトのタイムアウトをオーバーライドできます。