この記事について
前提
同じ時点のデータを2ルーチンが同時に更新してしまうパターン
事象
- 更新前
- 送金元:380
- 送金先:390
- 送金元から送金先へ10ドル送付するという処理を2ルーチンで動かす
- 更新後
- ルーチン1
- 送金元:370(380-10)
- 送金先:400(390+10)
- ルーチン2
- 送金元370 ※想定360
- 送金先410(400+10)
原因
- select時に2ルーチンで同じ時点(値が370時点)の送信元データを取得してしまっているため
解決策
- SELECT文に
FOR UPDATE
を追加することで、あるルーチンでデータ取得している際は、そのルーチンが COMMIT
か ROLLBACK
するまでブロックされ処理が止まる
SELECT * FROM accounts
WHERE id = $1 LIMIT 1
FOR UPDATE;
外部キー制約が原因でクエリの実行がブロックされるパターン
事象
- 更新用にaccounts取得のクエリを実行したところ、transfers tableのレコード 挿入が原因でクエリの実行がブロックされる
// ルーチン2で実行し、ブロックされたクエリ
SELECT * FROM accounts
WHERE id = $1 LIMIT 1
FOR UPDATE;
// ルーチン1で実行し、ブロック原因になっているクエリ
INSERT INTO transfers (
from_account_id,
to_account_id,
amount
) VALUES (
$1,
$2,
$3
) RETURNING *;
原因
- 前提として、
transfers
tableのfrom_account_id
と to_account_id
は、accounts
tableの外部キーとなっている。
transfers
tableの更新が行われるトランザクション実行中に、万が一accounts
tableのidが書き換えられてしまうと、不整合なtrasferレコードを作成してしまうことになる。
- それを防ぐため
transfers
tableの実行中はaccounts
tableの更新ができない
- 動画ではルーチン1が更に
SELECT * FROM accounts・・・
を発行し、ルーチン2でそれ以前に実行されたINSERT INTO transfers
によりブロック、一方でルーチン2はルーチン1によってブロックされているという状態つまりdeadlock状態となっていた
解決策
- SELECT文に
FOR NO KEY UPDATE
を追加することで、キーの更新が行われないことをPostgreSQL
へ伝える事ができ、ブロックされなくなる
SELECT * FROM accounts
WHERE id = $1 LIMIT 1
FOR NO KEY UPDATE;
後日追記