DBの勉強を進めると、避けなければならない処理として、デッドロックに出会います。
なんとなく理屈はわかるけど、あまりリアルに感じられない、という人もいるかも知れません。
そんな人の一助となるべく、どんな具体的な処理で発生するのか、まとめてみました。
#デッドロックとは
###デッドロックの定義
二つのトランザクションが,互いに相手の所有するロックが解除されるのを待機して処理が進まなくなる状態のこと。
基礎から理解するデータベースのしくみ(12)
https://tech.nikkeibp.co.jp/it/article/COLUMN/20060118/227490/
ここでいうトランザクションとは、DBMSがデータベースにデータを反映させる単位で、一連の処理(レコードの更新等)のまとまりです。データベースの整合性を保つために利用されます。
たとえば、レコードの更新の途中で処理が失敗した場合では、トランザクション単位で処理を取り消すことにより、不完全なデータがデータベースに残ること防ぎます。
###デッドロックが発生するロジック
以下の2つのトランザクションがあるとします。
- テーブルAとテーブルBを順に操作する、トランザクション(1)
- テーブルBとテーブルAを順に操作する、トランザクション(2)
この2つのトランザクションが同時期に実行されることを想定します。
- トランザクション(1)が、テーブルAをロックしテーブルBを更新しようとする。
- トランザクション(2)が、テーブルBをロックしテーブルAを更新しようとする。
タイミングが同時期であった場合、
トランザクション(1)は、トランザクション(2)によってロックされたテーブルBのロックが解除されるのを待ち、
トランザクション(2)は、トランザクション(1)によってロックされたテーブルAのロックが解除されるのを待ちます。
しかし、ロックはトランザクションが完了するまで解除されないので、ロックが解除されることはなく、それぞれのトランザクションは待ちつづけることになります。
これがデッドロックのロジックです。
※デッドロックはいくつかの種類に分類されますが、上記のロジックは「サイクルデッドロック」と呼ばれる、デッドロックの中でも最も典型的なパターンになります。
#具体例1
###ソース
メルカリJPのサーバーサイドエンジニアの@Hiraku氏が以下のブログで紹介されている。
###考察
上記「デッドロックが発生するロジック」に当てはめてみると
- トランザクション(1) ⇒ 出品者(A)・購入者(B)の情報を順に更新する処理(1)
- トランザクション(2) ⇒ 出品者(B)・購入者(A)の情報を順に更新する処理(2)
上記トランザクションは、顧客が異なるだけの同じトランザクション。
操作対象は
- テーブルA ⇒ 顧客Aの出品・購入の情報
- テーブルB ⇒ 顧客Bの出品・購入の情報
具体的なレコードについては言及されていませんが、(勝手に)想像してみて、この情報を、取引が発生したときに更新される「出品件数」と「購入件数」をもつ顧客に紐づいたレコードと考えてみます。
トランザクション(1)は、顧客Aのレコードをロックして、出品件数を更新。そのまま顧客Bのレコードにある購入件数を更新するために顧客Bのレコードにアクセスしようしますが、顧客Bのレコードはトランザクション(2)が出品件数を更新するためにロックしているので、ロック解除を待たなければなりません。
一方で、トランザクション(2)は、顧客Bの出品件数を更新したあと、顧客Aの購入件数にアクセスするが、すでにトランザクション(1)が顧客Aのレコードをロックしているので、解除を待っている状態になっています。
デッドロックが発生しますね。
#具体例2
https://tech.nikkeibp.co.jp/it/article/COLUMN/20060118/227490/
###考察
上記「デッドロックが発生するロジック」に当てはめてみると
- トランザクション(1) ⇒ 振込元の口座(A)・振込先の口座(B)の情報を順に更新する振込み処理(1)
- トランザクション(2) ⇒ 振込元の口座(B)・振込先の口座(A)の情報を順に更新する振込み処理(2)
上記トランザクションは、口座が異なるだけの同じトランザクションです。
操作対象は
- テーブルA ⇒ 口座(A)のレコード
- テーブルB ⇒ 口座(B)のレコード
口座(A) から口座(B)への振込み処理(トランザクション(1))が発生すると、まず口座(A)をロックして口座(A)から振込み金額を減算します。そののち口座Bのレコードに振り込み金額を加算するために口座(B)をロックしようとします。しかし、口座(B)はトランザクション(2)が振込み元として先にロックしているので、ロックの解除を待つことになります。
一方でトランザクション(2)は、口座(B)の更新が終わったあと、口座(A)を更新しようとしますが、すでに口座(A)はトランザクション(1)によってロックされているので、トランザクション(1)による口座(A)のロック解除を待つことになります。
互いにロックした口座の解除を待ち続けるのでデッドロックが発生します。
#デッドロックへの対応
デッドロックを回避するための代表的な対策には以下があります。
- トランザクションを作成する際のルールとして、アクセスするテーブル・レコードの順番を事前に決定しておく。
- トランザクションを長くせず、テーブル・レコードがロックされる時間を短くする。
- DBMSによるデッドロックの検知を適切に設定し、解析のために適切なログを取得しておく。
など
#まとめ
具体的なトランザクションとテーブル・レコードでデッドロックを確認しました。
デッドロックが、よりリアルに発生する不具合、と感じていただけたならば幸いです。
複数のトランザクションが同時実行される環境では、デッドロックは完全に避けることは、今もなお困難ですので。