MySQLのデッドロックを直そう

はじめに

俺がプログラミングが下手なせいかデッドロックがたまに起きる。対処法としてよくあるのがリトライしようとかロック順序を揃えようとかだが、どうしても後手に回ってしまうのが実情だ。

そこで事後的にどうにかする機会がたまに発生するのだが、リトライではなく根本的な直し方を解説する。

直し方

簡潔に説明すると以下の通りだ

  1. show engine innodb statusを出してデッドロック発生箇所を突き止める
  2. 手元でクエリを発行しながらdata_locksテーブルを眺め、機序を突き止める
  3. クエリを直す

簡単だね

SHOW ENGINE INNODB STATUS

この記事に確認方法が書かれている。

MySQL(InnoDB)でのDeadLock調査 - Qiita

直近で発生したデッドロックがどのトランザクションによるものかが分かる。ここで対処法が明確にわかる場合もあればそうでない場合もある。分からなければ後続のdata_locksを見る方法を推奨する

data_locks

MySQL :: MySQL 8.0 リファレンスマニュアル :: 27.12.13.1 data_locks テーブル

data_locksテーブルにはトランザクションによるロック状況が記録されている。適当にクエリを投げつつ、途中でdata_locksの中身を確認すれば、どこで何がどのようにロックされているかが分かる。

show engine~でもざっくり何が起こっているかは分かるが、もっと詳しく見たいだとか、ロックの種類に慣れ親しんでいないため自分の推論に不安があるとかの場合、これを見るのが確実。

MySQLのロックに関するドキュメントを読み込んでこれを見れば、どのようにデッドロックが起きているか言葉で説明できるようになるはずだ。あとはうまい具合にデッドロックが起きないように修正すればOK

デッドロックを直した時の話

最後にエピソードを一つ。MySQLでテーブルを結合してSELECT FOR UPDATEすると、結合先のテーブルまでロックされてしまう。Railsの一括読み込みのインターフェイスを利用しつつロックを取ったところこの罠を踏み、結合先のレコードをロックしたことによるデッドロックが発生した。

この時はshow engine~してもよく分からず(だってそこがロックされるなんて思わなかったんだもん)、data_locksを見て初めてJOIN先のロックが悪さをしていることに気がついた。FOR UPDATEだと排他ロックがかかるが、この時は共有ロックであればデッドロックが起こらない状況だったので、そうすることで済んだ。ロックの種類による競合の仕方とかも把握しておくと良い。

また小技として、JOIN先をロックしたくなければサブクエリを利用するといい方法がある。外側でSELECT FOR UPDATEしていても、サブクエリで参照されるテーブルにはロックがかからないことを利用している。

innodbでサブクエリを使ったときの FOR UPDATE のロックの範囲 - ngyukiの日記