スキップしてメイン コンテンツに移動

COMMITについて少し考えてみた(2)

前回、COMMITの失敗時にトランザクションの成否は未確定で、タイミングによっては成功している場合もあるし、失敗している場合もあることを検証しました。
今回は、そのような不安定なトランザクションの状態を確認する方法としてOracle 12c R1からサポートされたトランザクションガードがどのようなものが確認してみます。
前回のCOMMIT問題のまとめ

トランザクションガードの前提

トランザクションガードはOracle 12c R1以降でサポートされます。また、次に示すクライアント・ドライバをサポートしています。
  • 12c JDBCタイプ4(Thin)ドライバ
  • 12c OCI、OCCIクライアント・ドライバ
  • 12c Oracle Data Provider for .NET (ODP.NET)、管理対象外ドライバ
  • 12c ODP.NET、ODAC 12c リリース4以降の管理対象ドライバ
詳細はデータベース開発ガイドを確認してください。

トランザクションガードを使うための準備

今回は、JDBC(Thin)ドライバを使ってトランザクションガードの動きを確認してみますがいくつか準備を実施します。

トランザクションガードに対応するサービスの作成(変更)

以下のようなSQLでサービスを作成しますが、COMMIT_OUTCOME=TRUEおよびRETENTION_TIMEOUTを適切なサイズにしてサービスを作成(変更)する必要があります。(以下はサービスを作成しています)
 DECLARE
     PARAMETER_ARRAY DBMS_SERVICE.SVC_PARAMETER_ARRAY;
 BEGIN
     PARAMETER_ARRAY('COMMIT_OUTCOME'):='true';
     PARAMETER_ARRAY('RETENTION_TIMEOUT'):=604800;
     DBMS_SERVICE.CREATE_SERVICE(
         'TX_GUARD','TX_GUARD',PARAMETER_ARRAY);
     DBMS_SERVICE.START_SERVICE('TX_GUARD');
 END;
 /


サンプルのJDBCプログラム

トランザクションガードを利用したJDBCプログラムの流れは以下のような感じになります。(コード全体は最後に記載)
  • トランザクションガードに対応したサービスへのconnectionオブジェクトから論理トランザクションIDを取得
  • 論理トランザクションIDの取得は、ドライバ提供のAPI (JDBCにはgetLTXID、OCIにはLTXID、OCI_ATTR_GET、およびODP.NETにはLogicalTransactionId)を使用して、前に失敗したセッションから(も)論理トランザクションIDを取得
  • 取得した論理トランザクションIDを使用してDBMS_APP_CONT.GET_LTXID_OUTCOME PL/SQLプロシージャを起動
  • 戻り状態により 、ドライバは最後のトランザクションがCOMMITTED (TRUE/FALSE)およびUSER_CALL_COMPLETED (TRUE/FALSE)であったことを確認可能

前回と同じ方法でCOMMITエラーを発生させてみる

REDOログへの書き込み完了前に障害が発生した場合

1) INSERTを実行
 $ java TG
 INSERTING
 INSERTED
 commit to enter any key
2) log writerのsemtimedop(2)をブロック(上記シーケンス図の②の部分)
 $ gdb -p 1870
 ...
 (gdb) catch syscall semtimedop
 (gdb) c
3) COMMITを実行
 commit to enter any key

 COMMITTING
上記の状態でCOMMITが完了することはありません
4) この状態でlog writerの障害を発生(kill)させてみます
 $ kill -9 4006
また、2)のgdbのセッションをキャンセルします
 (gdb) q
5) サンプルプログラムにエラーが返ります
 COMMITTING
 ERROR
  MESSAGE: No more data to read from socket
 If you want to check the transaction status, wait for completion of crash recovery.
6) クラッシュリカバリ後にトランザクションガードの状態を確認します
 If you want to check the transaction status, wait for completion of crash recovery.

 TX STATUS: UNCOMMITTED <- コミットはされていない
7) データが存在しないことを確認します
 SQL> select * from sample_table;

 no rows selected

今度はREDOログへの書き込み完了後に障害が発生した場合

1) INSERTを実行
 $ java TG
 INSERTING
 INSERTED
 commit to enter any key
2) log writer workerのsemctl(2)をブロック(上記シーケンス図の④の直前部分)
 $ gdb -p 4041
 ...
 (gdb) catch syscall semctl
 (gdb) c
3) COMMITを実行
 commit to enter any key

 COMMITTING
上記の状態でCOMMITが完了することはありません
4) この状態でlog writer workerの障害を発生(kill)させてみます
 $ kill -9 4041
また、2)のgdbのセッションをキャンセルします
 (gdb) q
5) サンプルプログラムにエラーが返ります
 COMMITTING
 ERROR
  MESSAGE: No more data to read from socket
 If you want to check the transaction status, wait for completion of crash recovery.
6) クラッシュリカバリ後にトランザクションガードの状態を確認します
 If you want to check the transaction status, wait for completion of crash recovery.

 TX STATUS: COMMITTED <- コミットは完了している
7) データが存在することを確認します
 SQL> select * from sample_table;

  ID
 ----------
   1

まとめ

COMMITの失敗によるトランザクションの成否はタイミングにより異なることを2回に渡って見てきました。このためCOMMITの失敗によるトランザクションのリトライを行うと比較的簡単にデータの論理破壊が発生します。より信頼性の高いアプリケーションを構築するためには特定のトランザクションがデータベースサーバーとして確定しているか否かを知ることが重要です。Oracleを使う場合はトランザクションガードが有効に機能するのでミッションクリティカルなアプリケーションで比較的簡単にトランザクションの行方を知りたい方は使ってみてください。

この動きはPostgreSQLでも気になる

今回はOracleでCOMMITの動きとCOMMIT失敗におけるトランザクションの成否の判定にトランザクションガードが使えることを見てきましたが、ふとPostgreSQLの場合はどうなんだろうと思ったので、次回はPostgreSQLで同様の検証をしてみます。

サンプルのJDBCプログラム

 import java.util.Scanner;
 import java.sql.*;
 import oracle.jdbc.pool.*;
 import oracle.jdbc.*;

 public class TG {
     static void waitForAnyKey(String msg) {
         System.out.println(msg);
         Scanner scan = new Scanner(System.in);
         String key = scan.nextLine();
     }

     public static void main(String argv[]) throws
     SQLException {
         String url = "jdbc:oracle:thin:@//localhost/TX_GUARD";
         OracleDataSource ods=new OracleDataSource();
         ods.setURL(url);
         ods.setUser("ユーザー");
         ods.setPassword("パスワード");
         OracleConnection conn = (OracleConnection)ods.getConnection();
         conn.setAutoCommit (false);
         LogicalTransactionId ltxid = conn.getLogicalTransactionId();
         try {
             System.out.println("INSERTING");
             conn.prepareStatement("insert into sample_table values (1)")
                 .execute();
             System.out.println("INSERTED");
             waitForAnyKey("commit to enter any key");
             System.out.println("COMMITTING");
             conn.prepareStatement("commit").execute();
             System.out.println("COMMITED");
         } catch (SQLException e) {
             System.out.println("ERROR");
             System.out.println(" MESSAGE: "+ e.getMessage());
             waitForAnyKey("If you want to check the transaction status, "
                          +"wait for completion of crash recovery.");
             OracleConnection conn2 = (OracleConnection) ods.getConnection();
             CallableStatement c = conn2.prepareCall(
                 "declare "
                 +"      b1 boolean; "
                 +"      b2 boolean; "
                 +"begin "
                 +"      DBMS_APP_CONT.GET_LTXID_OUTCOME(?,b1,b2); "
                 +"       ? := case when B1 then 'COMMITTED' "
                 +"                 else 'UNCOMMITTED' end; "
                 +"end;");
             c.setObject(1, ltxid);
             c.registerOutParameter(2, OracleTypes.VARCHAR);
             c.execute();
             System.out.println("TX STATUS: "+ c.getString(2));
         }
     }
 }

コメント