27 Java用のトランザクション・ガード

Oracle Database 12cリリース1 (12.1)では、トランザクション・ガード機能を導入し、計画済および計画外の停止と重複発行の際に最大1回の実行のための汎用インフラストラクチャを提供します。この章では、次の項で、Java用のトランザクション・ガードについて説明します。

27.1 Java用のトランザクション・ガードの概要

現在のアプリケーションでは、保証されていて拡張性のある方法で最後のコミット操作の結果を判断する方法やサーバーとの通信エラーの後の処理は未解決の問題です。多くの場合、エンドユーザーは、重複したリクエストの再発行を回避するために特定のステップに従うように要求されます。たとえば、一部のアプリケーションでは、ユーザーに送信ボタンを2回クリックしないように警告します。これは、2回クリックした場合、ユーザーが意図せずに、同じものを2回購入し、2回分の支払いを行うことがあるためです。

この問題を解決するため、Java用のトランザクション・ガードではトランザクションが重複発行されても1件のみが有効になります。つまり、すべてのトランザクションの実行は最大1回であるため、アプリケーションは重複するトランザクションを送信できません。すべてのトランザクションは論理トランザクションID(LTXID)でタグ付けされており、これは、トランザクションが障害の前にコミットしたかどうかを確認するために、障害の発生後に、アプリケーションによって使用されます。たとえば、コミット・コールが戻らない場合、アプリケーションはLTXIDを使用して、コールが正常終了したかどうかを検出できます。

Java用のアプリケーション・コンティニュイティ機能はJava用のトランザクション・ガードを内部で使用しているため、透過的なセッション・リカバリと、処理中のトランザクションの初めからSQL文(問合せおよびDML)のリプレイが可能になります。アプリケーション・コンティニュイティによって計画済または計画外の停止発生後に作業のリカバリが可能になり、Java用のトランザクション・ガードによってトランザクションは重複発行されても必ず1件のみが有効になります。停止が発生すると、リカバリでは、障害が発生する前と同じ状態がリストアされます。

27.2 XAトランザクションのためのトランザクション・ガードのサポート

Oracle Database 12cリリース2 (12.2.0.1)以降、トランザクション・ガードでは、1フェーズ・コミットの最適化、読取り専用最適化および昇格可能なXAのためのXAトランザクションがサポートされています。XAを使用したトランザクション・ガードでは、XAトランザクションでのリカバリ可能な停止に続く安全なリプレイを提供します。XAサポートを追加すると、トランザクション・マネージャは、トランザクション・ガードによる冪等性を備えたリプレイを簡単に行えるようになります。

ノート:

接続プールからセッションをチェックアウトする際にXAを使用したトランザクション・ガードを使用するには、データベースのバージョンがOracle 12cリリース2 (12.2.0.1)であり、トランザクション・ガードが有効になっていることを確認する必要があります。

新しいサーバー・プロトコルでは、コミットがデータベースによって1フェーズ管理されている場合にコミット結果が保証され、トランザクション・マネージャがそのセッションのトランザクションを協調させたときに、無効化されたモードに切り替えます。新しいプロトコルでは、LTXIDにステータス・フラグを設定し、このフラグにより、現在のトランザクション所有者に基づいてLTXIDへのアクセスが有効化および無効化されます。

このプロトコルは、インテリジェントに処理を行い、XAは1つのXAトランザクションに対して多数のセッションおよびブランチを網羅できます。さらに、1つのブランチが保留されると、元のトランザクションはアクティブなまま、1つのセッションが別のトランザクションに対して使用可能になります。元のブランチを作成した同じセッションまたはRACインスタンスでXAトランザクションを準備またはコミットするための要件はありません。XA用のトランザクション・ガードでは、1フェーズXAトランザクションのデータベースでコミット結果を処理するための次の2つの新しい方法を使用し、一方で、トランザクション・マネージャは2フェーズ・トランザクションのコミット結果の処理を続行します。

  • 最初の方法を使用すると、リカバリ可能なエラーまたは他の指定された条件がそのセッションで発生するまで、ドライバはLTXIDを暫定とマークします。リカバリ可能なエラー(または他の条件)が発生すると、クライアントのLTXIDは最終とマークされます。保証されたコミット結果が提供されるのは、LTXIDがクライアントで最終の場合、およびLTXIDのステータスがVALIDである(データベースがそのトランザクションを所有していることを示す)サーバーで最終の場合のみです。他のアクセス試行はエラーが戻されます。

  • 2番目の方法を使用すると、リカバリ可能なエラーまたは他の指定した条件がそのセッションで発生するまで、クライアント・ドライバはLTXIDをアプリケーションに提供しません。

27.3 XAによるトランザクション・ガードの使用方法

この項では、次の項について説明します。

昇格可能なXAを使用したコミット結果の取得

ローカル・トランザクションの場合、リカバリ可能な例外があると、リクエストではLTXIDをトランザクション・キーとして取得します。2番目のブランチが開始されると、リクエストはXAに昇格されるか、XAに変換され、グローバル・トランザクション識別子(GTRID)がこれに割り当てられます。コミットの処理中にリカバリ可能な停止が発生し、アプリケーションがトランザクション・マネージャから応答を受信しない場合、アプリケーションはトランザクション・マネージャに結果を要求します。データベースへのほとんどのリクエストでは、ローカル・トランザクションまたは単一ブランチの最適化のいずれかを使用します。ローカル・トランザクションまたは昇格可能なXAを使用している場合、XAに対するラウンドトリップおよび管理でオーバーヘッドはありません。これは、ほとんどのトランザクションがローカルであるためです。これらのトランザクションのワークフローは次のとおりです。

  1. XAに変換する前は、トランザクション処理はローカルです。認証、SELECT文およびローカル・トランザクションが実行されると、ローカル・トランザクションのLTXIDが使用されます。

  2. トランザクション・マネージャがGTRIDをトランザクションに割り当てるのは、トランザクションの2番目のブランチをオープンしたためにXAの使用を開始した場合のみです。

  3. リカバリ可能なエラーの後に、アプリケーションでコミット結果を受信しない場合、トランザクションがローカルであれば、トランザクション・マネージャはLTXIDをGET_LTXID_OUTCOMEプロシージャとともに使用してコミット結果を取得し、アプリケーションにCOMMITTEDまたはUNCOMMITTEDの結果を戻します。

昇格可能なXAが追加された場合のリプレイ

昇格される前に、昇格可能なXAでは、静的XAでサポートされていないコールと設定を使用してRDBMSコミットをサポートしています。これらのコールには、自動コミット・モード、PL/SQLに組み込まれたDDL、DCL、COMMIT、およびリモート・プロシージャ・コールを使用したCOMMITが含まれています。これらのユーザー・コールおよびモードに対するCOMMIT結果はRDBMSで制御され、エラーの後、トランザクション・ガードを使用してコミット結果を確認できます。

昇格されるまで、トランザクション・マネージャではリクエストがCOMMITで発行されたかどうかを認識していません。トランザクション・マネージャはリカバリ可能なエラーの後でリクエストをリプレイする場合に、RDBMS COMMITが発生したかどうかを判定する必要があります。RDBMS COMMITが発生したか発生する可能性がある場合、リプレイは発生しません。GET_LTXID_OUTCOMEプロシージャでは現在のトランザクション結果の報告しか行われないため、これを判定する際には不十分です。LTXIDが変更されると、トランザクションがコミットされます。したがって、LTXIDのコールバックの起動により、トランザクションがコミットされたことが示されます。

27.4 Java API用のトランザクション・ガード

この項では、次のアクティビティのためにJava用のトランザクション・ガードに関連付けられたAPIについて説明します。

27.4.1 論理トランザクションIDの取得

サーバーによって送信される現在の論理トランザクションIDを取得するには、oracle.jdbc.OracleConnectionインタフェースのgetLogicalTransactionIdメソッドを使用します。このメソッドをコールしても、データベースのラウンドトリップは発生しません。

      OracleConnection oconn = (OracleConnection) ods.getConnection();
      ...
      // Getting the 1st LTXID after connecting
      LogicalTransactionId firstLtxid = oconn.getLogicalTransactionId();

27.4.2 更新済論理トランザクションIDの取得

論理トランザクションIDに対する更新を受信するには、oracle.jdbc.LogicalTransactionIdEventListenerインタフェースを使用します。論理トランザクションIDイベントを処理するために、このインタフェースをアプリケーションに実装する必要があります。

27.4.2.1 イベント・リスナーの登録

リスナーを論理トランザクションIDイベントに登録するには、addLogicalTransactionIdEventListenerメソッドを使用します。

      OracleConnection oconn = (OracleConnection) ods.getConnection();
      ...
      // The subsequent LTXID updates can be obtained through the listener
      oconn.addLogicalTransactionIdEventListener(this);

addLogicalTransactionIdEventListener(LogicalTransactionIdEventListener listener, java.util.concurrent.Executor executor)メソッドを使用して、リスナーをエグゼキュータに登録することもできます。

27.4.2.2 イベント・リスナーの登録解除

リスナーを論理トランザクションIDイベントから登録解除するには、removeLogicalTransactionIdEventListenerメソッドを使用します。

      OracleConnection oconn = (OracleConnection) ods.getConnection();
      ...
      // The subsequent LTXID updates can be obtained through the listener
      oconn.removeLogicalTransactionIdEventListener(this);

27.5 完全な例: トランザクション・ガードAPIの使用

次に、トランザクション・ガードAPIを使用する完全な例を示します。

      import oracle.jdbc.pool.OracleDataSource;
      import oracle.jdbc.OracleConnection;
      import oracle.jdbc.LogicalTransactionId;
      import oracle.jdbc.LogicalTransactionIdEvent;
      import oracle.jdbc.LogicalTransactionIdEventListener;
      
      public class transactionGuardExample
      {
            ...
            ...
            OracleDataSource ods = new OracleDataSource();
            ods.setURL(url);
            ods.setUser("user");
            ods.setPassword("password");
 
            OracleConnection oconn = (OracleConnection) ods.getConnection();
 
            // Getting the 1st LTXID after connecting
            LogicalTransactionId firstLtxid = oconn.getLogicalTransactionId();
 
            // The subsequent LTXID updates can be obtained via the listener
            oconn.addLogicalTransactionIdEventListener(this);
        }
 
      public class LtxidListenerImpl
        implements LogicalTransactionIdEventListener
      {
        ...
 
        public void onLogicalTransactionIdEvent(LogicalTransactionIdEvent ltxidEvent)
        {
          LogicalTransactionId newLtxid = ltxidEvent.getLogicalTransactionId();
          // process newLtxid ......
        }
      }

27.6 サーバー側トランザクション・ガードAPIの使用について

DBMS_APP_CONTパッケージには、サーバー側トランザクション・ガードAPIが含まれているGET_LTXID_OUTCOMEプロシージャが含まれています。このプロシージャを実行すると、トランザクションの結果が出力されます。トランザクションがコミットされていない場合、偽のトランザクションがコミットされます。そうでない場合、トランザクションの状態が戻されます。デフォルトでは、このパッケージのEXECUTE権限がデータベース管理者に付与されています。

構文

  PROCEDURE GET_LTXID_OUTCOME(CLIENT_LTXID        IN  RAW,
                              committed           OUT BOOLEAN,
                              USER_CALL_COMPLETED OUT BOOLEAN);

入力パラメータ

CLIENT_LTXIDは、クライアント・ドライバのLTXIDを指定します。

出力パラメータ

COMMITTEDは、トランザクションがコミットされることを指定します。

USER_CALL_COMPLETEDは、トランザクションをコミットしたユーザー・コールが完了したことを指定します。

例外

サーバーがクライアントよりも先行している場合、SERVER_AHEADがスローされます。したがって、トランザクションは古くなり、すでにコミットされている必要があります。

クライアントがサーバーよりも先行している場合、CLIENT_AHEADがスローされます。これは、サーバーがフラッシュバックされるか、LTXIDが破損している場合にのみ、発生することがあります。これらの状況のいずれかの場合、結果は判定できません。

処理中にエラーが発生した場合、ERRORがスローされ、結果は判定できません。現在のプロシージャの実行中に発生したエラー・コードを指定します。

例27-1に、GET_LTXID_OUTCOMEプロシージャをコールし、LTXIDの結果を確認する方法を示します。

例27-1 LTXIDの結果の確認

    ...
    OracleConnection oconn = (OracleConnection) ods.getConnection();
    LogicalTransactionId ltxid = oconn.getLogicalTransactionId();
    boolean committed = false;
    boolean call_completed = false;
 
    try
    {
      CallableStatement cstmt = oconn.prepareCall(GET_LTXID_OUTCOME);
      cstmt.setObject(1, ltxid);
      cstmt.registerOutParameter(2, OracleTypes.BIT);
      cstmt.registerOutParameter(3, OracleTypes.BIT);
 
      cstmt.execute();
 
      committed = cstmt.getBoolean(2);
      call_completed = cstmt.getBoolean(3);
 
      System.out.println("LTXID committed ? " + committed);
      System.out.println("User call completed ? " + call_completed);
    }
    catch (SQLException sqlexc)
    {
      System.out.println("Calling GET_LTXID_OUTCOME failed");
      sqlexc.printStackTrace();
    }