MySQLのBULK INSERTでデッドロックを回避する
Railsでactiverecord-importを使ってバルクインサートをする時にDeadlockエラーが出たので対処しました。バルクインサートをする時にはINSERT順を気をつけないといけませんねという話です。
MySQLのBULK INSERTでエラー発生
ログ集計した結果をテーブルにBULK INSERTしていたら
ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: INSERT INTO `books` (`id`,`name`) VALUES ...
というデッドロックエラーが発生しました。
BULK INSERTのエラー原因調査
ということでデッドロックの原因を調べてみたのですが、インサート順がインデックスに沿っていないことがまずかったようです。(参考: mysql deadlocks with concurrent inserts)
デッドロックが発生してしまうケース
例えば以下のような場合にまずいことになります。
まず、テーブルが
| id | name |
|---|---|
| 1 | ActiveRecordの本 |
| 2 | PostgreSQLの本 |
となっていて、nameカラムがUNIQUEキーになっていたとします。
ここで、booksテーブルに対してプロセスAとプロセスBが
INSERT INTO `books` (`id`,`name`) VALUES (NULL,'Rubyの本'),(NULL,'Capybaraの本');INSERT INTO `books` (`id`,`name`) VALUES (NULL,'MySQLの本'),(NULL,'Railsの本');というBULK INSERT処理を同時に実行しようとするとデッドロックが起こる場合があります。
デッドロックが発生するまでの処理内容
デッドロックは以下のような処理順の場合に発生してしまいます。
- プロセスAが
Rubyの本をINSERTするためにPostgreSQLの本からRubyの本までのギャップロックA1を取得 - プロセスBが
MySQLの本をINSERTするためにActiveRecordの本からMySQLの本までのギャップロックB1を取得 - プロセスBが
Railsの本をINSERTするためにPostgreSQLの本からRailsの本までのギャップロックB2を取得しようとする- プロセスAが取得している
PostgreSQLの本からRubyの本までのギャップロックA1の解放を待つ
- プロセスAが取得している
- プロセスAが
Capybaraの本をINSERTするためにActiveRecordの本からCapybaraの本までのギャップロックA2を取得しようとする- プロセスBが取得している
ActiveRecordの本からMySQLの本までのギャップロックB1の解放を待つ
- プロセスBが取得している
図にすると
| レコード | プロセスA | プロセスB |
|---|---|---|
| ActiveRecordの本 | ギャップロックB1 | |
| Capybaraの本 | ギャップロックA2 | ‖ |
| MySQLの本 | ギャップロックA2 | ギャップロックB1 |
| PostgreSQLの本 | ギャップロックA1 | ギャップロックB2 |
| Railsの本 | ‖ | ギャップロックB2 |
| Rubyの本 | ギャップロックA1 |
という感じになります。
ご覧の通り
- プロセスAがギャップロックB1の解放を待つ
- プロセスBがギャップロックA1の解放を待つ
という形でデッドロックが発生してしまいます。
デッドロックの回避方法
このようなデッドロックはBULK INSERTする時の順番を気をつければ回避できます。
今回の場合だと
INSERT INTO `books` (`id`,`name`) VALUES (NULL,'Capybaraの本'), (NULL,'Rubyの本');INSERT INTO `books` (`id`,`name`) VALUES (NULL,'MySQLの本'),(NULL,'Railsの本');とすればOKです。
こうすると今回の例のような場合でも
| レコード | プロセスA | プロセスB |
|---|---|---|
| ActiveRecordの本 | ギャップロックB1 | |
| Capybaraの本 | ギャップロックA1 | ‖ |
| MySQLの本 | ギャップロックA1 | ギャップロックB1 |
| PostgreSQLの本 | ギャップロックA2 | ギャップロックB2 |
| Railsの本 | ‖ | ギャップロックB2 |
| Rubyの本 | ギャップロックA2 |
という感じになって解放待ちのギャップロックを辿ってもループしなくなるのでデッドロックを回避できます。
ということでBULK INSERTの際はINSERTの順番に気をつけましょう。