JDBCによる悲観ロックの落とし穴

提供: tknotebook
2016年7月30日 (土) 04:09時点におけるNakamuri (トーク | 投稿記録)による版

移動: 案内検索

メインページ>コンピュータの部屋#Java>Java Tips


あるあるですが、JDBCを使って悲観ロックでレコードを更新する場合、たまにこんなコードを見かけることがあります。


更新するテーブルの定義

CREATE TABLE ACCOUNT
(
   NAME varchar(12) PRIMARY KEY NOT NULL,
   BALANCE int NOT NULL
)


更新するコード

rsA = s.executeQuery("select name, balance from account where name='A' for update");
if (rsA.next()) {
    balanceA = rsA.getInt("balance");
    logger.debug("balanceA = " + balanceA);
} else {
    throw new Exception("行がありません");
}
s.executeUpdate(String.format("update account set balance=%d where name='A'", balanceA - 100));
logger.debug("update A");

conn.commit();

実は Isolation level が READ COMMITED では、これは悲観ロックになっていないのです。


コードでは、

  1. 更新モードでカーソルを取得
  2. カーソルの行の値を取得し更新ロックをかける。
  3. 行の値を更新し排他ロックをかける。

となっていて問題なさそうに見えますが、問題点は更新ロックの寿命です。

多くのDBでは、行ロックの更新ロックは、Isolation Level が READ COMMITED の場合、カーソルの指す行のみがロックされ、 カーソルが別の行へ移ると元の行の更新ロックは解除されます。カーソルがクローズされれば当然更新ロックは残りません。

Statement オブジェクトは executeXXX を実行すると、カーソルをクローズするため、上記のコードでは レコードが排他ロックするまでのわずかな隙間時間に、レコードのロックがない期間が存在するのです。これではうまく動きません。

対処方法は3つあります。

1. Isolation Level を REPEATABLE READ 以上にする。

2. カーソルを取得せず update account set balance=balance-100 where ... の一行でレコードを更新する

3. 以下のように、カーソルを使って行を更新する。

rsA = s.executeQuery("select name, balance from account where name='A' for update");
if (rsA.next()) {
    balanceA = rsA.getInt("balance");
    logger.debug("balanceA = " + balanceA);
    rsA.updateInt("balance", balanceA - 100);
    rsA.updateRow();
    logger.debug("update A");
} else {
   throw new Exception("行がありません");
}
conn.commit();


JDBCを長く使っておられる方でも、いろいろ制約はあるものの、カーソルで更新ができることを知らない人が結構います。 Updatable Cursor を活用しましょう。