この記事について
いいコードはいい心から
今回はmysqlの罠についてです。便利なものほど疑えという格言が似合う体験をしたのでまとめました。
触れるのはON DUPLICATE KEY UPDATE 句
についてです。INSERTとUPDATEを自動で振り分けてくれるとても便利な句ですが、実は罠が潜んでいます。
どんな罠なのか、、、、
なんと、ON DUPLICATE KEY UPDATE
を含んだクエリを実行するとデッドロックがさらに発生しやすくなるというものです。
そのため、実装は避ける必要がありそうです。
今回はこのようなことがわかった経緯についてまとめました。
ON DUPLICATE KEY UPDATEとは
- 13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE ステートメントより
行を挿入すると、UNIQUE インデックスまたは PRIMARY KEY で値が重複する場合、古い行の UPDATE が発生します。つまり圃場UUIDが重複している場合はデータを更新してくれるというもの
です。
例えば、以下のようなテーブルがあったとする
// userテーブル、ulidはPRIMARY KEYとする
ulid | first_name | last_name |
---|---|---|
01FVSHW3S63T5D5Q8KTT132RK3 | Japan | Taro |
01FVSHW3S7HW03J702MAE82MQS | Japan | Jiro |
01FVSHW3SER8977QCJBYZD9HAW | Japan | Saburo |
そこに以下のようなクエリが実行されたとする
INSERT INTO user ( ulid , first_name , last_name ) VALUES ( '01FVSHW3S99VWCKTQVG1EQB6CM', 'US', 'John' ), ( '01FVSHW3SER8977QCJBYZD9HAW', 'US', 'SAM' );
この場合、ulidが01FVSHW3SER8977QCJBYZD9HAW
のレコードはすでにテーブルに存在するため、エラーとなってしまう。
そのため、SQL側を実行する前にインサートするデータを選抜しなければならない。少し面倒ですよね
そんな時にON DUPLICATE KEY UPDATE
句の出番。以下のように書き換えてみる
INSERT INTO user ( ulid , first_name , last_name ) VALUES ( '01FVSHW3S99VWCKTQVG1EQB6CM', 'US', 'John' ), ( '01FVSHW3SER8977QCJBYZD9HAW', 'US', 'SAM' ) ON DUPLICATE KEY UPDATE ulid = VALUES(ulid) , first_name = VALUES(first_name) , last_name = VALUES(last_name);
その結果、新規のレコードはインサートされ、既存のレコードは更新されます
ulid | first_name | last_name |
---|---|---|
01FVSHW3S63T5D5Q8KTT132RK3 | Japan | Taro |
01FVSHW3S7HW03J702MAE82MQS | Japan | Jiro |
01FVSHW3SER8977QCJBYZD9HAW | US | SAM |
01FVSHW3S99VWCKTQVG1EQB6CM | US | John |
とっても便利なので、よく使われてました。足下に火がついているとも知らずに。。。。
罠
とある日にバグが見つかりました。データが登録できていないという状況でした。サーバーのログを見てみるとDeadLock
が発生しており、サーバー側で500エラーが発生していました。
さらに詳しく調べていくことにしました。
調査方法
DeadLock
が発生している状況を作り出しつつ、その際にどんなクエリが叩かれているかを確認することにしました。そのためにgeneral.log, slow_query_logをon
にしました。またlong_query_time
を0にし、全ての処理のログを出すようにしました。
(long_query_time
で設定された 秒数 より処理時間が長いとスロークエリログの対象になる。)
mysql> set global general_log = on; mysql> set global slow_query_log=1; mysql> set global long_query_time=0;
その結果以下のようなログが出てきました
2023-04-04T02:52:08.644934Z 1847 Query INSERT INTO テーブル名 ( 省略 ) VALUES ( 省略 ),( 省略 ) ON DUPLICATE KEY UPDATE 省略
ON DUPLICATE KEY UPDATE
句を含むクエリがログに出てきた。ON DUPLICATE KEY UPDATE
に関して調べてみると興味深い記事が出てきました。
Bug #98324 Deadlocks more frequent since version 5.7.26という報告によると、5.7.26 以降にアップグレードすると、ON DUPLICATE KEY UPDATE
を含んだクエリでデッドロックがさらに発生するそうです。
マイナー バージョンを 5.7.25 から 5.7.26 以降にアップグレードすると、さらに多くのデッドロックが発生します。 REPLACE ステートメントを使用した単純なテスト ケースでは、アップグレード後に約 20 倍のデッドロック メッセージが発生します。 5.7.26 での変更がおそらく関連しているのではないかと思います。
13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE ステートメントにも以下のように記載れています。
複数の一意キーまたは主キーを持つテーブルに対する INSERT ... ON DUPLICATE KEY UPDATE ステートメントも安全でないとマークされます。 (Bug #11765650、Bug #58637)
今回事象が発生した際のmysqlのバージョンは5.7.38
であったため、デッドロックが発生しやすい環境でした。。。
実際に動作確認し、DeadLockが起こることを確認しました。
begin; INSERT INTO hoge_table ( 省略 ) VALUES ( 省略 ),( 省略 ) ON DUPLICATE KEY UPDATE 省略; commit;
- ターミナルを2つ準備する。
- ターミナルAでは
while true;do mysql -u root database_1 < a.sql; done
でSQLを無限ループさせる - ターミナルBでは
while true;do mysql -u root database_1 < b.sql; done
でSQLを無限ループさせる
その結果、以下のようなログが出力されました
$ while true;do mysql -u root database_1 < a.sql; done ERROR 1213 (40001) at line 3: Deadlock found when trying to get lock; try restarting transaction
大量に insert into on duplicate update
が走った際にDeadlockが引き起こされることが確認できました。
対応方針
ON DUPLICATE KEY UPDATE
句が使えないため、アプリケーション側で制御する必要があります。
今回は2つの対応策を挙げました。
1. try catchで囲み、Exceptionを拾って、エラーをキャッチした場合はupdateの処理を実行するようにする
try { save(); } catch (e) { update(); }
2. update用の圃場データと、insert用の圃場データをselectで取得し、それぞれ処理を実行する
1の場合、圃場リストのデータを1つ1つsave or updateのクエリを叩く必要がある。これは処理速度の低下につながるため、避けたい。 2の場合は一括登録、一括更新することができる。
よって、2を採用することにしました。
最後に
今回の事象でmysqlにすこーーーーしだけ詳しくなった気がします。綺麗な花には棘がある
というように、疑うことも必要なのだなと改めて感じました。