[Oracle]デッドロックの発生と解消について

デッドロックって聞いたことがあっても自分で発生させることができる人ってどれくらいいるんでしょう?

■まずはRDBのACID特性とかのおさらい

リレーショナルデータベース(以降RDB)はACID特性があります。
詳しくは割愛します。
https://qiita.com/suziq99999/items/2e7037042b31a77b19c8
とか見るとまとめてくれている人がいるので見てみると良いと思います。

ここで大切なのはRDBはトランザクション処理を大切にしてくれていることです。
基本本当にありがたいですし、このトランザクション処理がないと、
金額計算とか2重更新とか結構システムがひどいことになってしまいます。

あるレコードを更新しているときはほかの処理で同じレコードを更新しようとしても
先に行った処理が完了するまで後からの処理は更新処理できないように制御(ロック)してくれます。

RDBのひとつの処理はcommitとかrollbackで制御します。
そのため、処理が行われるとcommitやrollbackをするまでそのレコードをロックし続け、
commitやrollbackが行われると初めてほかの処理を行うということがRDBで担保されいます。

コレが原因でたとえば先に行った処理の実行速度が遅かったりすると、
速度劣化してしまうといったこともありますが、
基本業務システムはこのACID特性に結構守られています。

ただ変な実装をしてしまうと、このデッドロックというものが発生してしまいます。

■デッドロックが発生してしまう原因

簡単な文章の説明ですと、
「2つの処理が互いに互いをロックし続ける」
という表現を使います。

図に起こします。

わかるかな?

1.まずAさんがxのデータに対して更新処理をかけます。
2.次にBさんがyのデータに対して更新処理をかけます。
3.Aさんはcommit処理等を行わずに、yのデータに対して、更新処理をかけます。
→このタイミングではBさんがyのデータに更新処理を行っているので、
ロックがかかっていてAさんは処理待ちになります。
4.Bさんがxのデータに対して更新処理をかけます。
→このタイミングではAさんがxのデータに更新処理を行っているので、
ロックがかかっていてBさんは処理待ちになります。
これでデッドロックの出来上がりです。

どちらの更新処理もロックされており、先に進むことが出来ず
ずっと互いの処理待ちになってしまいます。

コード的にはこんな感じになります。

"
package test.main;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class DeadLockTest {

  public static void main(String[] args) {
    Connection connA = null;
    Connection connB = null;

    try {
      //コネクション作成
      connA = makeConnection();
      connB = makeConnection();
      //自動コミットにしない
      connA.setAutoCommit(false);
      connB.setAutoCommit(false);
      //Aがデータxに対して更新
      update(connA, "x");
      //Bがデータyに対して更新
      update(connB, "y");
      //Aがデータyに対して更新
      update(connA, "y");
      //Bがデータxに対して更新
      update(connB, "x");

      //終了処理をする
      connA.commit();
      connB.commit();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } catch (SQLException e) {
      e.printStackTrace();
    } finally {
      try {
        if (connA != null) {
          connA.close();
        }
      } catch (SQLException e) {
        e.printStackTrace();
      }
      try {
        if (connB != null) {
          connB.close();
        }
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
  }

  private static Connection makeConnection() throws ClassNotFoundException, SQLException {
    /* ドライバクラスのロード */
    Class.forName("oracle.jdbc.driver.OracleDriver");
    return DriverManager.getConnection("url", "user", "pass");
  }

  private static void update(Connection conn, String updateKey) throws SQLException {
    PreparedStatement ps = null;
    int i = 1;
    try {
      ps = conn.prepareStatement(UPDATE_SQL);
      ps.setString(i++, updateKey);
      ps.executeUpdate();
    } finally {
      ps.close();
    }
  }

  private static final String UPDATE_SQL = "UPDATE table_name SET STR_2 = 1 WHERE STR_1 = ?";
}

■デッドロックしたらどうするの?デッドロックの解消法

待っていても解消しません。
いろいろ解消方法はありますがメジャーどころを(Oracle)、、、
とりあえずそのDBにsystemユーザーでログインしてください。
v$系のテーブル見れるユーザーです。

SELECT SID, SERIAL# FROM V$SESSION
WHERE SID IN (SELECT SID FROM V$LOCK WHERE TYPE IN (‘TM’,’TX’));

コレ投げるとロックしているセッションのSIDとSERIAL#が分かります。

Select V$SESSION.SID, SERIAL#, DBA_OBJECTS.OBJECT_NAME, V$SESSION.OSUSER, V$SESSION.PROGRAM
From V$LOCKED_OBJECT
Left Join DBA_OBJECTS on V$LOCKED_OBJECT.OBJECT_ID = DBA_OBJECTS.OBJECT_ID
Left Join V$SESSION ON V$LOCKED_OBJECT.SESSION_ID = V$SESSION.SID
Order By V$SESSION.SID, DBA_OBJECTS.OBJECT_NAME

とかかくともちょいいろいろ情報が見れます。どのテーブルにロックがかかっているかとか

で、ここでSID, SERIAL#がわかるので、
alter system kill session ‘sid, serial#’ immediate;
のSIDとSERIAL#の箇所に番号を埋め込んで実行しましょう。

これでデッドロックが解消します。

もちろんRDB自体の再起動でも解消しますが、そこまでやることでもないでしょう。

■デッドロックを防ぐには

手っ取り早いのはconnectionをいくつも使わないことですね。
1つのconnectionを利用していれば基本問題ないです。
あと複数ユーザーで利用するシステムの場合、一処理で同じテーブルに何回もアクセスしないこと。

これを守っていれば基本的にはデッドロックは防げるはずです。

ただcommitうちまくるとかは本当にやめたほうがいいです。rollback処理が出来なくなってしまいます。。。
setAutoCommit(true)も却下です。後輩にやられたら割と説教します。

こんなところでした。

行ロックとか表ロックについても書いてもいいかもしれませんねー

ではであ

コメント

タイトルとURLをコピーしました