改めて、DB2 for iでのレコードロックを検証していきたいと思います。
- (1) レコードロックとは?
- (2) RPGプログラムでレコードロックを発生させてみる
- (3) ロックが解放される条件
- (4) レコードロックをMONITOR文でキャッチする
- (5) デッドロックとは?
- (6) RPGプログラムでデッドロックを発生させてみる
- (7) 最大レコード待機時間の変更
- (8) SQLでのロック制御
(1) レコードロックとは?
レコードロックとは、ローリングストーンズ、ビートルズ、ジミヘンなどのLPのことで・・・違いました。それはロックレコードですね。。。
テーブルのとある行をこれから書き込みを行う作業前に、その対象レコードを「今から俺が使うよ!」という宣言のようなもので、レコード(行)をロック(占有)することです。
これにより、ロックされたレコードは、他のユーザーやバッチプログラムなどからの更新処理を一時的にブロックすることができます。
書き込み直前にロックをし、レコードの値を更新して対象レコードを更新する。そんな流れになります。
ただし、ロック事態が排他制御をしてくれるわけではなく、排他制御はプログラムできちんと実装する必要があります。(JavaのJPAでは、悲観的ロックや楽観的ロックという手法もあります)
(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)を付けずに実行すれば、ロック読み取りが実施されます!
検証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で使ったソースに、例外処理を加えてみます。
検証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プログラムでデッドロックを発生させてみる
処理の流れは、下記の図のようになります。
検証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文によっては大量のレコードをロックしてしまう点でしょうか。
以上で、レコードロック検証についてのまとめ終了です。