何が起きたのか
たまーにproduction環境でデッドロックが発生した
デッドロック発生時のログや各処理ごとで実行されるSQLのログから調査して
デッドロックが発生するクエリは特定できたがstagingやローカルでは再現せず
環境 | DB |
---|---|
production | MySQL互換Aurora |
staging | MySQL |
development | dockerのMySQL |
各環境は上記の状態だったため
Aurora独自の何かがあるのでは?と思い検証をしてみることに
デッドロック発生後の対処
デッドロック発生時、mysqlコマンドでDBにつないで
原因となるクエリをKILLしてみたが解決せず
リードレプリカをフェイルオーバーさせることで対応した
検証開始
デッドロックが起きていたテーブル群
- offers
- offer_child_1
- offer_child_2
- documents
- document_offer_child_2
※わかりやすいようにテーブル名は実際とは変えています
スキーマ
CREATE TABLE offers (
id bigint(8) AUTO_INCREMENT PRIMARY KEY,
title text
);
CREATE TABLE offer_child_1 (
id bigint(8) AUTO_INCREMENT PRIMARY KEY,
title text,
offer_id bigint(8),
FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE offer_child_2 (
id bigint(8) AUTO_INCREMENT PRIMARY KEY,
offer_id bigint(8),
FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE documents (
id bigint(8) AUTO_INCREMENT PRIMARY KEY,
url text
);
CREATE TABLE document_offer_child_2 (
id bigint(8) AUTO_INCREMENT PRIMARY KEY,
document_id bigint(8),
offer_child_2_id bigint(8),
FOREIGN KEY (document_id) REFERENCES documents(id),
FOREIGN KEY (offer_child_2_id) REFERENCES offer_child_2(id)
);
デッドロックが起きていたトランザクションのクエリ
事前に流すクエリ
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
INSERT INTO offer_child_2 (offer_id) VALUES (1);
INSERT INTO documents(url) VALUES ('http://example.com');
トランザクション
No | トランザクション1 | トランザクション2 |
---|---|---|
BEGIN; |
BEGIN; |
|
1 | UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; |
|
2 | SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE; |
|
3 | INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1); |
|
4 | INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a'); |
なぜデッドロックが起きるのか
InnoDBの行レベルロックには共有ロック(IS)
と排他ロック(IX)
の2種類がある
共有ロック(IS)
同士では競合は発生しないがそれ以外の組み合わせでは競合(ロック待ち)が発生する
IS | IX | |
---|---|---|
IS | 競合 | |
IX | 競合 | 競合 |
そして今回のケースでいうと
No.1
のUPDATE
文は該当のレコードにIX
を獲得
No.2
のSELECT FOR UPDATE
文は該当のレコードIX
を獲得
No.3
のINSERT
文は外部キーの参照先(documents
とoffer_child_2
)のレコードにIS
を獲得
No.4
のINSERT
文は外部キーの参照先(offers
)のレコードにIS
を獲得
となるため
No.3
はNo.2
をロック待ち、No.4
はNo.1
をロック待ちをして
トランザクション1、2間でロックが交錯しデッドロックが発生する
各環境での挙動
事前にパラメータグループから
innodb_lock_wait_timeout = 10
lock_wait_timeout = 15
autocommit = 0
innodb_lock_wait_timeout = 10
innodb_rollback_on_timeout = 0
innodb_table_locks = 1
lock_wait_timeout = 15
に設定しておく
innodb_rollback_on_timeout
とinnodb_table_locks
は
Auroraにパラメータグループで設定できないためAuroraでのデフォルト値に準拠
SHOW VARIABLES;
で確認可能
autocommit
についてはAuroraのデフォルトが0で
デッドロック検知には0がいいらしいのでMySQL側も0に設定してAuroraに合わせる
ref: https://dev.mysql.com/doc/refman/5.6/ja/innodb-deadlock-detection.html
MySQL互換のAurora(5.7.12)
mysql> BEGIN;
Query OK, 0 rows affected (0.05 sec)
mysql> UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
mysql> BEGIN;
Query OK, 0 rows affected (0.03 sec)
mysql> SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
+----+----------+
| id | offer_id |
+----+----------+
| 1 | 1 |
+----+----------+
1 row in set (0.09 sec)
mysql> INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
transaction1
もtransaction2
もタイムアウトせず止まり続け
INSERTのプロセスが残ったままDBのCPUが100%に貼りつく
MySQL(5.7.16)(5.7.12がなかったので仕方なく。。。)
mysql> BEGIN;
Query OK, 0 rows affected (0.02 sec)
mysql> UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
Query OK, 1 row affected (0.90 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.03 sec)
mysql> SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
+----+----------+
| id | offer_id |
+----+----------+
| 1 | 1 |
+----+----------+
1 row in set (0.07 sec)
mysql> INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
transaction1
のINSERT INTO document_offer_child_2...
は最初待たされるが
transaction2
のINSERT INTO offer_child_1...
を実行すると
transaction2
は瞬時にdeadlockが検知されてトランザクションがロールバックされて
transaction1
の待ちがなくなりINSERTを完了する
Auroraの他のパターンのデッドロックでの検知の挙動を検証
※見やすくなるようSQLのみ書いていく 実行順はここまで同様transaction1/2交互
長いので折りたたんだ
さらに
IX
のDMLは SELECT FOR UPDATE
/ UPDATE
IS
のDMLは INSERT
/ SELECT LOCK IN SHARE MODE
とすると、それぞれ2パターンとして
1つのインテンションロックの組み合わせの中でクエリの組み合わせは16パターン
全部で256(=16*16)
パターンになる
また、IX
にDELETE文も考慮するとクエリの組み合わせが36パターン
になり
全部で576(16*36)パターン
になってさすがにつらいので網羅せず
少しずつ条件を変えて検知できないデッドロックに作用してそうなクエリを探す
1. transaction1の先発クエリもSELECT FOR UPDATE
にしてみる
結果: × デッドロックを検知せず止まり続ける
考察: 先発のUPDATE
は関係なさそう
BEGIN;
SELECT * from offers WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
2. transaction2の先発クエリもUPDATE
にしてみる
結果: × デッドロックを検知せず止まり続ける
考察: 先発のSELECT FOR UPDATE
は関係なさそう
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
BEGIN;
UPDATE offer_child_2 SET offer_id = 1 WHERE id = 1;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
3. transaction1の後発クエリをSELECT LOCK IN SHARE MODE
にしてみる
結果: ◯ transaction1はデッドロックを検知してロールバックし、transaction2は正常終了
考察: 後発が両トランザクションINSERT
じゃないと検知しないデッドロックにならない?
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
4. transaction2の後発クエリをSELECT LOCK IN SHARE MODE
にしてみる
結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 3とはtransaction2がロールバックされた、InnoDBの仕様(※1)か
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
※1 https://dev.mysql.com/doc/refman/5.6/ja/innodb-deadlock-detection.html
小さいトランザクションを選択してロールバックしようと試みます
とあるので3のパターンではtransaction1が、今回はtransaction2が小さい
と判断されたのだろう
ここまでで
- transaction1/2いずれも後発が
INSERT
の時に検知されないデッドロックになることがわかった - では後発がいずれも
SELECT
/UPDATE
/DELETE
のときはどうだろうか
5. transaction1/2の後発クエリをSELECT LOCK IN SHARE MODE
にしてみる
結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 後発がSELECT
だとデッドロック検知してくれるらしい
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
6. transaction1/2の後発クエリをUPDATE
にしてみる
結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 後発がUPDATE
でもデッドロック検知してくれるらしい
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
UPDATE offer_child_2 SET offer_id = 1 WHERE id = 1;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
7. transaction1/2の後発クエリをDELETE
にしてみる
結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 後発がDELETE
でもデッドロック検知される INSERT
だけがやはり特殊なのか
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
DELETE from offer_child_2 WHERE id = 1;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
DELETE from offers WHERE id = 1;
8. transaction1/2の先発クエリをINSERT
にしてみる
結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: INSERT
が後発であることに意味があるらしい
BEGIN;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
結果
Auroraでは、transaction1/2ともに後発のクエリがINSERT
の時に発生するデッドロックが
検知されず止まり続けることがわかった
また、検知されないパターンのデッドロックをMySQLで試すと検知される
気になることがあったので追加で検証
- そもそもデッドロックではなく普通に
INSERT
がロック待ちした時ならちゃんとタイムアウトするのか -
UPDATE
の外部キー参照時の挙動はINSERT
と異なるのか
AuroraとMySQLのINSERT
とUPDATE
のロック待ちの挙動を検証
長いので折りたたんだ
9. IX
獲得されたレコードを参照するINSERT
をする
Aurora: 待ちが発生 タイムアウトもせず
MySQL: 待ちが発生 タイムアウトもせず
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
外部キーを持つレコードへのUPDATE
時の待ちの挙動を検証
10. UPDATE対象レコードの外部キーは変更せずリレーションがないカラムを更新する
Aurora: 待ちなしで更新できた
MySQL: 待ちなしで更新できた
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
-- id確認のSELECT
-- SELECT * FROM offer_child_1 WHERE offer_id = 1;
BEGIN;
UPDATE offer_child_1 SET title = CURRENT_TIMESTAMP WHERE id = 1;
11. UPDATE対象レコードの外部キーを変更すると
Aurora: 待ちが発生 タイムアウトもせず
MySQL: 待ちが発生 タイムアウトもせず
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
-- id確認
-- SELECT * FROM offers;
BEGIN;
SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;
-- id確認のSELECT
-- SELECT * FROM offer_child_1 WHERE offer_id = 1;
BEGIN;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;
12. デッドロック発生するクエリの後発クエリを、ロックされているレコードへ外部キーを変更するUPDATE
にする
Aurora: デッドロック検知されず止まり続ける
MySQL: デッドロックを検知してロールバック
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
INSERT INTO offer_child_2 (offer_id) VALUES (2);
offer_child_1のid=1
は10の検証で、
offersのid=2
は11の検証で、
すでに作ったからここでは作らない
-- id確認
-- SELECT * FROM document_offer_child_2 WHERE offer_child_2_id = 1;
BEGIN;
SELECT * from offers WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE document_offer_child_2 SET offer_child_2_id = 2 WHERE id = 1;
-- id確認
-- SELECT * FROM offer_child_1 WHERE offer_id = 1;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;
結果
MySQLでもAuroraでも外部キーの参照先がIX
を取られていると
INSERT
とUPDATE
(外部キー変更を含む時のみ)はロック待ちの時、タイムアウトしない
そして後発クエリがINSERT
だけでなく、UPDATE
(外部キー変更を含む時のみ)の場合も
Auroraはデッドロックを検知してくれない(MySQLはデッドロック検知してくれる)
さらに2点気になったので追加で検証
ref: https://dev.mysql.com/doc/refman/5.6/ja/innodb-foreign-key-constraints.html
- InnoDBの
INSERT
時の挙動は外部キー参照先にIS
を取るという処理はAuroraも同じだろうか - だったら
IS
獲得されたレコードに対する外部キー参照のINSERT
は待たずに実行されるよね?
あと
ref: https://dev.mysql.com/doc/refman/5.6/ja/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
-
INSERT
時にinnodb_lock_wait_timeout
が効かないってことは参照先をテーブルロックしようとしている可能性もある?
ってことでこれらも一応検証してみる
長いので折りたたんだ
13. 親レコードのIS取って、それを参照するINSERT
を書いてみる
Aurora: 待たずにINSERT
ができた
MySQL: 待たずにINSERTができた
考察: INSERT
時の外部キー参照先に取るロックはIS
で間違いなさそう
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
14. INSERT
したあと、そのレコードの外部キー参照先のテーブルの別レコードをIX
でロックしてみる
Aurora: 待たずにSELECT
できた
MySQL: 待たずにSELECT
できた
考察: つまり参照先がテーブルロックの可能性もない
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
BEGIN;
SELECT * FROM offers WHERE id = 2 FOR UPDATE;
結果
予想通り、かつInnoDBのドキュメント通り外部キーの参照先へのロックはIS
だった
ここまでの結果をまとめて考察すると
INSERT
時は外部キー参照先にはIS
(行レベル共有ロック)を取るだけ外部キー変更のないUPDATE
であれば外部キー参照先にIS
取らないので参照先がロック取られてても待ちなく更新可能- MySQLでもAuroraでも、外部キーの参照先が先にロックが取られているときは
外部キー参照を持つINSERT
や外部キーの変更があるUPDATE
のロック待ちはタイムアウトしない - そしてAuroraでは後発クエリがそのような
INSERT
やUPDATE
の時に発生するデッドロックはDBは検知してくれない - MySQLならそのようなデッドロックでも検知してロールバックしてくれる
ということがわかった
MySQL互換Auroraでの外部キーを持つINSERT
や外部キーの変更を行うUPDATE
のロック待ちの挙動が危うそうなのでアプリケーションレイヤーでも実装時に以下のようなことに気をつけた方がいいかもしれない
- 今回の事象の解決法としては
transaction2
がINSERT
の外部キー参照先(offers
)を最初にロック取りに行けばデッドロックは起きなくなる- ロックを取る順番を合わせるっていう基本的なデッドロック対策ですね
- 外部キー参照先の
IS
取ればシンプルに今回のデッドロックが解決 -
IX
だと同時にINSERT
やUPDATE
できなくなって同時アクセス時に待ちが発生する - どっちを取るかはケースバイケース
- 外部キー参照先の
-
INSERT
やUPDATE
だけのトランザクションであれば気にする必要はないが他のレコードもロックするような場合は注意が必要 - 親レコードを更新して子レコード(外部キー持っている)更新するような処理があると
INSERT
/UPDATE
側が親をロックするようにする以外に回避策なさそう?
- ロックを取る順番を合わせるっていう基本的なデッドロック対策ですね
- 中間テーブルのような外部キー参照が多いテーブルが多く、そこへの
INSERT
や外部キー変更のUPDATE
も多く、さらにその参照先もロックを取る処理があるアプリケーションは気をつけて実装しないと検知されないデッドロックで死ぬ可能性が高そう - スループットは落ちるがあえてMySQLを使うと少し安全かもしれない
ちなみにRailsでも共有ロック(IS
)取れる
ただし、MySQL依存になってちょっと悲しい
Model.lock('LOCK IN SHARE MODE').find(...)
ref: https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
タイムアウトしないとかデッドロック検知しない問題、何か解決法とか知ってる方いたらぜひコメントで教えて頂けると超助かります
※「このパラメータを設定すれば解決するよ」とかいうオチだったら恥ずかしすぎる。。。
おまけ編 PostgreSQL互換Auroraでも検証
長いので折りたたんだ
スキーマ
CREATE TABLE offers (
id SERIAL PRIMARY KEY,
title text
);
CREATE TABLE offer_child_1 (
id SERIAL PRIMARY KEY,
title text,
offer_id bigint,
FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE offer_child_2 (
id SERIAL PRIMARY KEY,
offer_id bigint,
FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
url text
);
CREATE TABLE document_offer_child_2 (
id SERIAL PRIMARY KEY,
document_id bigint,
offer_child_2_id bigint,
FOREIGN KEY (document_id) REFERENCES documents(id),
FOREIGN KEY (offer_child_2_id) REFERENCES offer_child_2(id)
);
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
INSERT INTO offer_child_2 (offer_id) VALUES (1);
INSERT INTO documents(url) VALUES ('http://example.com');
15. 最初のデッドロックのパターンを試す
Aurora: transaction2の後発クエリでロック待ちが発生しなかった
Postgres: transaction2の後発クエリでロック待ちが発生しなかった
考察: Postgresは行ロックの種類が多くUPDATE
とINSERT
が競合しないようだ
ref: https://www.postgresql.jp/document/9.6/html/explicit-locking.html#locking-rows
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
16. デッドロック再現のためUPDATE
をSELECT FOR UPDATE
に変える
Aurora: デッドロックを検知した
Postgres: デッドロックを検知した
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
17. 後発クエリを外部キー変更のあるUPDATEにしてみる
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
INSERT INTO offer_child_2 (offer_id) VALUES (2);
Aurora: デッドロックを検知した
Postgres: デッドロックを検知した
BEGIN;
SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE document_offer_child_2 SET offer_child_2_id = 2 WHERE id = 1;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;
18. SELECT FOR UPDATE
へのINSERT
の待ちを検証
lock_timeout
を設定したかったがパラメータグループに項目がなく設定できなかったのでコンソールから設定
(これAWS RDSだと設定永続化できないのかな。。。)
postgresの`lock_timeoutはmsecなので3000(=3sec)で
Aurora: 3秒でロックがタイムアウトした
Postgres: 3秒でロックがタイムアウトした
SET lock_timeout = 3000;
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
SET lock_timeout = 3000;
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
19. SELECT FOR UPDATE
へのUPDATE
の待ちを検証
Aurora: 3秒でタイムアウトした
Postgres: 3秒でタイムアウトした
SET lock_timeout = 3000;
BEGIN;
SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;
SET lock_timeout = 3000;
BEGIN;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;(3)
結果
Postgresの方がロックのモードが多彩で暗黙的なロックで競合しづらいようなので安心かもしれない
また、Postgres互換のAuroraやPostgresでは
外部キーが絡んだINSERT
やUPDATE
でのロック待ちはちゃんとタイムアウトするし
デッドロックが検知されない問題も起きなかった
結論
パターンとして見落としがあるかもしれないのでこれで完璧とは言えないが
- MySQL互換のAuroraもMySQLも外部キーが絡んだ
INSERT
やUPDATE
でタイムアウトしないロック待ちが発生する - MySQL互換のAuroraではさらにその
INSERT
/UPDATE
によるデッドロックが検知されない - Postgres互換のAuroraやPostgresは
INSERT
/UPDATE
のデッドロック検知されたし- 外部キーが絡んだ
INSERT
やUPDATE
による待ちもちゃんとタイムアウトする - 行ロックの種類が多いのでMySQLよりもそもそも競合しづらい
- 外部キーが絡んだ
デッドロック発生しやすさや発生時の挙動など考えて安全度が高そうなのは
Postgres互換Aurora = Postgres > MySQL > MySQL互換Aurora
といった感じかな
雑な追記
ふと検証してないことに気づいて
Aurora(MySQL互換)で外部キー参照先がロックされている状態でのDELETE
を試したところ
正常にロックタイムアウトされた
2トランザクション2クエリでデッドロックを起こすパターンも
後続クエリを外部キーを先にロックされたDELETE
にして試したが
両方のトランザクションともロックタイムアウトしたのでちょっと予想外だったけどまぁ問題ない挙動
やはりINSERT/UPDATEだけが異質