タイガー!タイガー!じれったいぞー!(SE編)

AS400, Java, JavaEE, JSF等の開発、習慣など。日々の気づきをまとめたブログ(備忘録)

【AS400】レコードロック検証

改めて、DB2 for iでのレコードロックを検証していきたいと思います。

(1) レコードロックとは?

レコードロックとは、ローリングストーンズ、ビートルズ、ジミヘンなどのLPのことで・・・違いました。それはロックレコードですね。。。
テーブルのとある行をこれから書き込みを行う作業前に、その対象レコードを「今から俺が使うよ!」という宣言のようなもので、レコード(行)をロック(占有)することです。
これにより、ロックされたレコードは、他のユーザーやバッチプログラムなどからの更新処理を一時的にブロックすることができます。
書き込み直前にロックをし、レコードの値を更新して対象レコードを更新する。そんな流れになります。
ただし、ロック事態が排他制御をしてくれるわけではなく、排他制御はプログラムできちんと実装する必要があります。(JavaJPAでは、悲観的ロックや楽観的ロックという手法もあります)

(2) RPGプログラムでレコードロックを発生させてみる

それでは、実際にレコードロックを発生させてみましょう。

サンプルで下記のようなテーブルを準備しました。従業員マスタは、レコードロック検証全体で使います。部門マスタはデッドロック検証で利用します。

■ 従業員マスタ (TIGERDB/EMPLOYEES)

社員CD 苗字 名前 電話番号 部門CD
0001 山田 太郎 012-3456-7890 10
0002 山田 花子 012-3456-7891 20

■ 部門マスタ (TIGERDB/SECTIONS)

部門CD 部門名
10 営業部
20 管理部

次のような対話型RPGを作成しました。1つの対話型JOBで「社員CD=0001」をロックし、もう1つの対話型JOBで同じ「社員CD=0001」をロックしてみます(検証1の図参照)。
ちなみに、ロック呼び出しの条件は、対象テーブル(EMPLOYEES)をUモードで定義し、READ、CHAIN文などでオプション(N)を付けずに実行すれば、ロック読み取りが実施されます!

f:id:no14141:20161121170325j:plain

検証1用ソースPGM RPGLESRC/SAMPLE39

H DATEDIT(*YMD/)                                                 
F****************************************************************
F* FILE 定義                                                     
F****************************************************************
F* 従業員マスタ                                                  
FEMPLOYEES UF   E           K DISK                               
F*                                                               
D****************************************************************
D* PROTOTYPE/INTERFACE                                           
D****************************************************************
D MAIN            PR                  EXTPGM('SAMPLE39')         
D MAIN            PI                                             
D*                                                               
D****************************************************************
D* VARIABLE 定義                                                 
D****************************************************************
D LOCK_ID         S              4A   INZ(*BLANK)                
D UNLOCK_FLG      S              1A                              
D W@MSSG          S             50    INZ(*BLANK)                
D*                                                               
C****************************************************************
C* MAIN                                                          
C****************************************************************
C                   EXSR      #MAIN                              
C                   EXSR      #FINAL                             
 /FREE                                                           
  //*************************************************************
  //*  MAIN                                                      
  //*************************************************************
  BEGSR #MAIN;                                                   
     DSPLY ' レコードIDを指定して下さい  ?' '' LOCK_ID;
     EMP_CD = LOCK_ID;
     CHAIN EMP_CD RECEMP; //←読み取り成功で、レコードロック!!
     IF %FOUND();
         W@MSSG = ' レコード読み取り中 :' + LOCK_ID;
         DSPLY W@MSSG;
     ELSE;
         W@MSSG = ' レコードが存在しません :' + LOCK_ID;
         DSPLY W@MSSG;
         LEAVESR;
     ENDIF;
      
   // ロック開放
      DSPLY '  ロックを開放しますか?  (Y/N)' '' UNLOCK_FLG;
      IF UNLOCK_FLG = 'Y';
          UNLOCK EMPLOYEES;
      ENDIF;
  ENDSR;
  //*************************************************************
  //* FINALIZE 
  //*************************************************************
  BEGSR #FINAL; 
      *INLR = *ON;
      RETURN; 
  ENDSR; 
 /END-FREE 

検証1 実行結果

従業員マスタレコード0001をUSER01のJOBがロックしている状態で、USER02がロックさせます。
すると、USER02では、初期値60秒を経過したのち、アベンド(プログラム異常終了)が発生してしまいました。

[USER01] CALL PGM(TIGEROBJ/SAMPLE39)   
[USER01] DSPLY   レコードIDを指定して下さい  ?
[USER01] ? 0001
[USER01] DSPLY   レコード読み取り中 :0001
[USER01] DSPLY    ロックを開放しますか?  (Y/N)   ※USER01は応答しないでおく。
[USER02] CALL PGM(TIGEROBJ/SAMPLE39)
[USER02] DSPLY   レコードIDを指定して下さい  ?
[USER02] ? 0001
[USER02] ロック Waiting -  60秒(デフォルト値)
[USER02] アベンド発生!「アプリケーション・エラー。 RNX1218 は, SAMPLE39 によって
         ステートメント 0000000044 命令 X'0000' で監視されていません。」
[USER02] プログラム異常終了
[USER01] ? Y     
[USER01] プログラム正常終了

レコードロックの確認方法

ロックが発生しているかどうかは、「DSPRCDLCKコマンド」で確認が可能です。
下記は、「DSPRCDLCK FILE(TIGERDB/EMPLOYEES)」を実行した結果になります。

                       メンバー・レコード・ロックの表示                         
                                                          システム :   AS400 
 ファイル . . . . . . :   EMPLOYEES       メンバー . . . . . . :   EMPLOYEES    
   ライブラリー . . . :     TIGERDB                                             
                                                                                
       レコード                                              ロック             
         番号      ジョブ      ユーザー    番号    状況      タイプ             
               1  USER01      USER01      701737   保留     UPDATE   ←ロックしているJOB      
                  USER02      USER02      701944   待機     UPDATE   ←ロック待機中のJOB

ロックウェイト時のWRKACTJOB

                               活動ジョブの処理 
                                                      
 オプションを入力して,実行キーを押してください。
   2= 変更   3= 保留     4= 終了   5= 処理   6= 解放   7=メッセージ の表示
   8=スプール・ファイル の処理   13= 切断 ...                              
                     現行                 
 OPT  サブシステム/ジョブ  ユーザー       タイプ  CPU %  機能             状況             
      QINTER         QSYS        SBS      .0                   DEQW   
        USER01       USER01      INT      .0  PGM-SAMPLE39     DSPW  
        USER02       USER02      INT      .0  PGM-SAMPLE39     LCKW  <<<<< Lock Waitの意味   

(3) ロックが解放される条件

ロックが解放される条件は、次の通りです。

(4) レコードロックをMONITOR文でキャッチする

レコードロックのついては、例外コード1218でキャッチすることが可能です。RPG言語では、MONITORで行います。
MONITOR命令については、以前のブログでまとめていました。

検証1で使ったソースに、例外処理を加えてみます。

f:id:no14141:20161121170349j:plain

検証2用ソースPGM RPGLESRC/SAMPLE39B

 BEGSR #MAIN;                                                
   MONITOR;                                                 
       DSPLY ' レコードIDを指定して下さい  ?' '' LOCK_ID; 
       EMP_CD = LOCK_ID;                                    
       CHAIN EMP_CD RECEMP;                                 
       IF %FOUND();                                         
           W@MSSG = ' レコード読み取り中 :' + LOCK_ID;      
           DSPLY W@MSSG;                                    
       ELSE;                                                
           W@MSSG = ' レコードが存在しません :' + LOCK_ID;  
           DSPLY W@MSSG;                                    
           LEAVESR;                                         
       ENDIF;                                               
   ON-ERROR 1218;                                           
       W@MSSG = ' レコードがロックされています :' + LOCK_ID;
       DSPLY W@MSSG;                                        
       LEAVESR;                                             
   ENDMON;                                                  
                                                            
 // ロック開放                                              
     DSPLY '  ロックを開放しますか?  (Y/N)' '' UNLOCK_FLG;     
     IF UNLOCK_FLG = 'Y';                                       
         UNLOCK EMPLOYEES;                                      
     ENDIF;                                                     
 ENDSR;                                                         
 //*************************************************************
 //* FINALIZE                                                   
 //*************************************************************
 BEGSR #FINAL;                                                  
     *INLR = *ON;                                               
     RETURN;                                                    
 ENDSR;                                                         
/END-FREE                                                       

検証2 実行結果

例外処理を組み込むことで、レコードロックを例外としてキャッチすることができます。
実施に実施してみると、アベンドではなく、ログを吐いて処理を継続してくれていることがわかります。

[USER01] CALL PGM(TIGEROBJ/SAMPLE39B)
[USER01] DSPLY   レコードIDを指定して下さい  ?
[USER01] ? 0001
[USER01] DSPLY   レコード読み取り中 :0001
[USER01] DSPLY    ロックを開放しますか?  (Y/N)   ※USER01は応答しないでおく。
[USER02] CALL PGM(TIGEROBJ/SAMPLE39B)  
[USER02] DSPLY   レコードIDを指定して下さい  ?
[USER02] ? 0001
[USER02] ロック Waiting -  60秒(デフォルト値)
[USER02] ログ出力「レコード 1 がジョブ 701737/USER01/USER01 で使用中。」
[USER02] ログ出力「ファイル EMPLOYEES のレコードを割り振ることができない。」
[USER02] DSPLY   レコードがロックされています :0001
[USER02] プログラム正常終了
[USER01] ? Y     
[USER01] プログラム正常終了
  • 例外をキャッチすれば、例外処理を記述する必要がありますが、正常終了になるため、上位からの呼び出しにも戻りコードをエラー扱いにするなどして、連携することが可能になります。

(5) デッドロックとは?

ロックといえば、「デッドロック」という概念が必ず出てきますね。
デッドロックとは、IT用語辞典を引用させていただきましょう。

デッドロックとは、複数のプロセスが互いに相手の占有している資源の解放を待ってしまい、処理が停止してしまうこと。データベースの排他制御の不備などが原因で起こる。(IT用語辞典

(6) RPGプログラムでデッドロックを発生させてみる

処理の流れは、下記の図のようになります。

20161121181628

検証PGMでは、パラメータを1で渡すと「従業員マスタ→部門マスタ」という順で呼び出してくれるのに対して、1以外の値をパラメータに渡すと、「部門マスタ→従業員マスタ」の順で呼び出されます。
すなわち、JOB別にそれぞれ別のテーブルのレコードをロックさせた状態で、次にすでにロックしているレコードを呼び合うことで、デッドロックが発生します。

検証3用ソースPGM RPGLESRC/SAMPLE39C

H DATEDIT(*YMD/) 
F****************************************************************
F* FILE 定義
F****************************************************************
F* 従業員マスタ
FEMPLOYEES UF   E           K DISK
F* 部門マスタ
FSECTIONS  UF   E           K DISK
F*
D****************************************************************
D* PROTOTYPE/INTERFACE 
D****************************************************************
D MAIN            PR                  EXTPGM('SAMPLE39C')
D                                1A   CONST
D MAIN            PI
DP@OPTION                        1A   CONST
D*
D****************************************************************
D* VARIABLE 定義
D****************************************************************
D EMP_LOCK_ID     S              4A   INZ(*BLANK)
D SEC_LOCK_ID     S              2A   INZ(*BLANK)
D UNLOCK_FLG      S              1A
D W@MSSG          S             50    INZ(*BLANK)
D*
 /FREE
  //*************************************************************
  //*  MAIN
  //*************************************************************
      IF P@OPTION = '1';
           EXSR  #SUB_EMP;
           EXSR  #SUB_SEC;
      ELSE;
           EXSR  #SUB_SEC;
           EXSR  #SUB_EMP;
      ENDIF;
      EXSR  #FINAL;
  
  //*************************************************************
  //*  従業員マスタ操作
  //*************************************************************
  BEGSR #SUB_EMP;
     DSPLY ' 従業員マスタIDを指定して下さい  ?' '' EMP_LOCK_ID;
     MONITOR;
         EMP_CD = EMP_LOCK_ID;
         CHAIN EMP_CD RECEMP;
         IF %FOUND();
             W@MSSG = ' 従業員レコード読み取り中 :' + EMP_LOCK_ID;
             DSPLY W@MSSG;
         ELSE;
             W@MSSG = ' 従業員レコードが存在しません :' + EMP_LOCK_ID;
             DSPLY W@MSSG;
             LEAVESR;
         ENDIF;
     ON-ERROR 1218;
         W@MSSG = ' 従業員レコードがロックされています :' + EMP_LOCK_ID;
         DSPLY W@MSSG;
         LEAVESR;
     ENDMON;
   // ロック開放
      DSPLY '  ロックを解除しますか?  (Y/N)' '' UNLOCK_FLG;
      IF UNLOCK_FLG = 'Y';
          UNLOCK EMPLOYEES;
      ENDIF;
  ENDSR;
  //*************************************************************
  //*  部門マスタ操作 
  //************************************************************* 
  BEGSR #SUB_SEC;
     DSPLY ' 部門マスタIDを指定して下さい  ?' '' SEC_LOCK_ID;
     MONITOR;
         SEC_CD = SEC_LOCK_ID;
         CHAIN SEC_CD RECSEC;
         IF %FOUND();
             W@MSSG = ' 部門レコード読み取り中 :' + SEC_LOCK_ID;
             DSPLY W@MSSG;
         ELSE;
             W@MSSG = ' 部門レコードが存在しません :' + SEC_LOCK_ID;
             DSPLY W@MSSG;
             LEAVESR;
         ENDIF;
     ON-ERROR 1218;
         W@MSSG = ' 部門レコードがロックされています :' + SEC_LOCK_ID;
         DSPLY W@MSSG;
         LEAVESR;
     ENDMON;

   // ロック開放
      DSPLY '  ロックを解除しますか?  (Y/N)' '' UNLOCK_FLG;
      IF UNLOCK_FLG = 'Y';
          UNLOCK SECTIONS;
      ENDIF;
  ENDSR;
  //*************************************************************
  //* FINALIZE
  //*************************************************************
  BEGSR #FINAL;
      *INLR = *ON;
      RETURN;
  ENDSR; 
 /END-FREE

検証3 実行結果

実際にデッドロックが発生させることができました。
先に例外をキャッチしたJOBが例外処理となり、後のJOBがレコードを読み取りできるようになります。 ということは、ロックをキャッチして、再度読み取りをトライ!などという処理をループ扱いにしてしまうと、永久に待ち合うことになってしまいます(おそろしや~)。

[USER01] CALL PGM(TIGEROBJ/SAMPLE39C) PARM('1') 
[USER01] DSPLY   従業員マスタIDを指定して下さい  ?
[USER01] ? 0001 
[USER01] DSPLY   従業員レコード読み取り中 :0001
[USER01] DSPLY    ロックを解除しますか?  (Y/N) 
[USER01] ? N 
[USER02] CALL PGM(TIGEROBJ/SAMPLE39C) PARM('2')
[USER02] DSPLY   部門マスタIDを指定して下さい  ? 
[USER02] ? 10
[USER02] DSPLY   部門レコード読み取り中 :01   
[USER02] DSPLY    ロックを解除しますか?  (Y/N) 
[USER02] ? N

[USER01] DSPLY   部門マスタIDを指定して下さい  ? 
[USER01] ? 10

[USER02] DSPLY   従業員マスタIDを指定して下さい  ?
[USER02] ? 0001 

<<デッドロック発生中!!>>
↓
※先に例外をキャッチしたJOBが例外処理となり、
  後のJOBがレコードを読み取りできるようになる。
(以下省略)

DSPRCDLCK

                       メンバー・レコード・ロックの表示                         
                                                          システム :   AS400
 ファイル . . . . . . :   EMPLOYEES       メンバー . . . . . . :   EMPLOYEES    
   ライブラリー . . . :     TIGERDB                                             
                                                                                
       レコード                                              ロック             
         番号      ジョブ      ユーザー    番号    状況      タイプ             
               1  USER01      USER01      701737   保留     UPDATE              
                  USER02      USER02      701944   待機     UPDATE              
                                                                          
-----------------------------------------------------------------------------------

                       メンバー・レコード・ロックの表示                         
                                                          システム :   AS400
 ファイル . . . . . . :   SECTIONS        メンバー . . . . . . :   SECTIONS     
   ライブラリー . . . :     TIGERDB                                             
                                                                                
       レコード                                              ロック             
         番号      ジョブ      ユーザー    番号    状況      タイプ             
               1  USER02      USER02      701944   保留     UPDATE              
                  USER01      USER01      701737   待機     UPDATE         

WRKACTJOB

                               活動ジョブの処理 
                                                      
 オプションを入力して,実行キーを押してください。
   2= 変更   3= 保留     4= 終了   5= 処理   6= 解放   7=メッセージ の表示
   8=スプール・ファイル の処理   13= 切断 ...                              
                     現行                 
 OPT  サブシステム/ジョブ  ユーザー       タイプ  CPU %  機能             状況             
      QINTER         QSYS        SBS      .0                   DEQW    
        USER01       USER01      INT      .0  PGM-SAMPLE39C    LCKW             
        USER02       USER02      INT      .0  PGM-SAMPLE39C    LCKW

デッドロックを発生させない定石として、トランザクション処理は、一定の順序でテーブルアクセスを行う」という点と、「読み取り成功するまでループ扱いにしない(例外処理を組み込む)」という2点でしょうか。

(7) 最大レコード待機時間の変更

DB2 for iでは、テーブル構築時に、最大レコード待機時間を指定できます。
デフォルト値が、60秒となっておりますので、よほどの事情が無ければこのままでいいように思います。

すでに運用中のテーブルの場合には、CHGPFコマンドで変更は可能です(取扱い注意! 私の環境では、CHGPF実行後にRPGのリコンパイルは不要でした)。
下記のコマンドでは、最大レコード待機時間を10秒に設定するケースです。

CHGPF FILE(TIGERDB/EMPLOYEES) WAITRCD(10)

(8) SQLでのロック制御

では、SQL文でのロックはどうするのか? ということで、DB2 for iでは「FOR UPDATE WITH 分離レベル」でロック処理ができました。

SELECT * FROM TIGERDB/EMPLOYEES 
 WHERE EMP_CD = '0001'  
   FOR UPDATE WITH RS    

分離レベルについては、データがアクセスされている間、 データが他のプロセスからどのようにロックされ分離されるかを決めるもので、複数指定ができます。きつめのロック、軽めのロックという具合でしょうか・・・。
こちらはJavaEEの場合、APサーバー側で設定し、EJBで自動トランザクション処理を行っているため、個別での操作は経験不足です。
いずれにしましても気をつけたい点は、COMMIT or ROLLBACKを実行するまで、ロックされ続けている点と、WHERE文によっては大量のレコードをロックしてしまう点でしょうか。

以上で、レコードロック検証についてのまとめ終了です。