MySQL InnoDBのギャップロックによるデッドロックを解明する
こんにちは。仮想通貨の損益計算ツール「Gtax」、仮想通貨の確定申告サポート「Guaridan」を提供するAerial Partnersのエンジニアインターンの伊藤です。
今回は、Aerial Partnersのチームが実際にどのような技術を用いてブロックチェーンの社会実装を進めているかをお伝えするために、僕が最近取り組んでいた、大量のレコードを捌くDB処理の実装において直面した問題についてお話させていただきます。
InnoDBのロックアーキテクチャはややこしい!
例題です。以下のような操作において、一体どのような原因でデッドロックが発生してしまうのでしょうか。
テーブル内容
> SHOW COLUMNS FROM t1;+-------+---------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+-------+---------+------+-----+---------+-------+| num | int(11) | NO | PRI | NULL | |+-------+---------+------+-----+---------+-------+> SELECT * FROM t1;+-----+| num |+-----+| 5 |+-----+
操作内容
A> BEGIN;A> DELETE FROM t1 WHERE num = 5; ...①
B> BEGIN;B> DELETE FROM t1 WHERE num = 5; ...②/* Bは[LOCK WAIT]状態になる */
A> INSERT INTO t1(num) VALUES(5); ...③
B> ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction/* Transaction AとTransaction Bの間でデッドロックが発生する! */
ここで、各クエリが同様にnum=5
の行に対する排他ロックをリクエストしたと考えると、デッドロックが発生した理由は全くわからなくなります。 ③の時点ではそれ以前ですでにTransaction Aがロックを取得しており、デッドロックは発生しないことになるからです。
原因の調査が難航した理由は、InnoDBのロックアーキテクチャの複雑性にありました。 実は、各クエリがそれぞれ異なる性質のロックをリクエストしたために、競合が発生してしまったのです!
デッドロック発生までの流れ
各クエリがリクエストしたロックを詳しく見ていくことで、どのようにデッドロックが発生したのかを解説します。
クエリ①
DELETE FROM t1 WHERE num = 5;
等価検索によるDELETEクエリです。この時、t1テーブルには検索条件に一致するレコードが存在します。よって、このクエリはnum=5
のインデックスレコードに対してレコードロックを取得します。
レコードロック
インデックスレコードに対するロック。 いわゆる行ロック。
クエリ②
DELETE FROM t1 WHERE num = 5;
構文は①と同じクエリです。しかし、こちらのクエリがリクエストするロックは、クエリ①のレコードロックと同じではありません!①によって削除されたあとに検索条件に従ってインデックスを走査した結果、レコードが存在しないので、ファントムリードを回避する目的でnum=5とその前(4)の間のギャップロックを取得しようとします。そして、num=5
のインデックスレコードにはロックがすでにかかっているので許可されず、ロックの解放を待ち始めます。
ギャップロック
レコードとレコードの間への挿入を停止するロックです。詳細は後述。
②のクエリがリクエストするロックと解放を待つロックがそれぞれ違う性質のものであることがわかります。
クエリ③
INSERT INTO t1(num) VALUES(5);
INSERTは挿入インテンションギャップロックと呼ばれるギャップロックの一種を取得しようと試みます。そして、②によるギャップロックと競合し、デッドロックが発生します。
挿入インテンションギャップロック
挿入の際に取得されるギャップロック。対象のギャップに対して他の挿入インテンションギャップロックがあっても、挿入するインデックスが異なる場合は相互にブロックしません。
なぜギャップロックが必要なのか
InnoDBのデフォルトのトランザクション分離レベルはREPEATABLE READ
であり、これはファントムリードとよばれる問題を許容する分離レベルです。
ファントムリードとは、他のトランザクションによってINSERTされたレコードが自分のトランザクションで見えてしまう現象のことです。これにより、同一トランザクション内で同様の条件で検索を行った場合でも、結果の一貫性が保証されなくなってしまいます。
InnoDBでは、REPEATABLE READ
のレベルにおいてもファントムリードを回避するために、ギャップロックを行うことがあります。 ギャップロックによって走査した範囲への挿入をロックすることで、ファントムデータが発生しないようになっています。
解決策
トランザクション分離レベルをREAD COMMITTED
に変更する
READ COMMITTED
にするとファントムリードを許容するため、ギャップロックは取得されず、デッドロックも発生しません。 ファントムリード、ファジーリードが問題とならない場合はこの方法もありですね。
想定外のギャップロックが発生し得るトランザクションを並列で発生させない
同インデックスに対してDELETE 後 INSERTするトランザクションが並列で発生する可能性が高い場合は、UPDATEで代替することを検討すると良いでしょう。
参考
- [InnoDB のレコード、ギャップ、およびネクストキーロック] ,https://dev.mysql.com/doc/refman/5.6/ja/innodb-record-level-locks.html
- [InnoDB のロックモード], https://dev.mysql.com/doc/refman/5.6/ja/innodb-lock-modes.html
- [MySQLのロックについて], http://d.hatena.ne.jp/sh2/20140914
終わりに
Aerialのメンバーと一緒に話してみたい、ブロックチェーン技術の社会実装に興味がある、という方はぜひ一度私たちとお話しにオフィスまで遊びに来てください!
エンジニア募集