投稿日:2026/5/9
更新日:2026/6/21

Webアプリは複数ユーザーが同時に使います。
この「同時実行」が原因で、まれにデータが壊れる不具合が起きます。これがレースコンディションです。
処理の順番次第で、結果が変わってしまう状態です。
たとえば在庫1個の商品に、同時に2人が注文した場合:
この結果、在庫がマイナスになることがあります。
「確認(SELECT)」と「更新(UPDATE)」を分けて書くと起きやすくなります。
-- 危険な例
SELECT stock
FROM products
WHERE product_id = 1;
-- アプリ側で stock > 0 を確認してから...
UPDATE products
SET stock = stock - 1
WHERE product_id = 1;
この書き方だと、AさんとBさんが同時に SELECT した時点では、どちらも「在庫あり」と判断できてしまいます。
レースコンディション対策は、次の順で考えると整理しやすいです。
一番シンプルで強い対策です。
「確認」と「更新」を分けず、更新条件にビジネスルールを入れます。
-- 在庫がある時だけ減らす(安全な例)
UPDATE products
SET stock = stock - 1
WHERE product_id = $1
AND stock > 0;
このSQLは、stock > 0 を満たす場合だけ更新します。
更新件数が 1 なら購入成功、0 なら在庫不足として扱います。
-- 残高が足りる時だけ引き落とす例
UPDATE accounts
SET balance = balance - 1000
WHERE account_id = $1
AND balance >= 1000;
このように、可能なら SELECTしてから判断するのではなく、UPDATEの条件で守る のが基本です。
アプリ側のバリデーションだけでは、実装漏れや直接SQL操作ですり抜ける可能性があります。
最終的にはDB制約で壊れたデータを拒否します。
代表的な制約:
UNIQUE: 重複登録を防ぐCHECK: 不正な値を防ぐFOREIGN KEY: 存在しない関連データを防ぐNOT NULL: 必須値の欠落を防ぐ-- 同じユーザーが同じイベントに重複申込できないようにする
ALTER TABLE event_entries
ADD CONSTRAINT event_entries_user_event_unique
UNIQUE (user_id, event_id);
この制約があれば、同時に2つの申込リクエストが来ても、最終的にDBが重複を拒否します。
-- 在庫がマイナスにならないようにする
ALTER TABLE products
ADD CONSTRAINT products_stock_non_negative
CHECK (stock >= 0);
DB制約は、レースコンディション対策の「最後の砦」です。
複数のSQLを1つのまとまった処理として扱いたい場合は、トランザクションを使います。
BEGIN;
UPDATE products
SET stock = stock - 1
WHERE product_id = $1
AND stock > 0;
INSERT INTO orders (user_id, product_id)
VALUES ($2, $1);
COMMIT;
途中で失敗した場合は ROLLBACK して、在庫だけ減って注文が作られないような中途半端な状態を防ぎます。
ただし、トランザクションを使うだけで全てのレースコンディションが消えるわけではありません。
トランザクション内でも「SELECTしてからUPDATE」のような危険な読み書きをすれば、分離レベルやロックの取り方によっては問題が残ります。
特定の行を読み、その後に同じ行を更新する必要がある場合は、行ロックを取ります。
BEGIN;
SELECT *
FROM products
WHERE product_id = $1
FOR UPDATE;
-- 在庫確認、注文作成など
UPDATE products
SET stock = stock - 1
WHERE product_id = $1;
COMMIT;
FOR UPDATE を使うと、同じ行を更新しようとする他のトランザクションは待たされます。
複雑な業務ロジックで、どうしても読み取り結果をもとに後続処理をしたい場合に使います。
注意点:
advisory lock は、DBが提供する「任意キーの排他制御」です。
行やテーブルに直接紐づかない処理を、同時に1つだけ実行したい場合に使います。
SELECT pg_try_advisory_lock(1001);
向いている用途:
在庫更新のように、特定の行の整合性を守るだけなら、まずは原子的UPDATEや行ロックを優先します。
advisory lock は「処理全体を同時に1つだけにしたい」ときに向いています。
| やりたいこと | 主な対策 |
|---|---|
| 在庫をマイナスにしたくない | 原子的UPDATE + CHECK制約 |
| 同じユーザーの重複申込を防ぎたい | UNIQUE制約 |
| 複数SQLをまとめて成功・失敗させたい | トランザクション |
| 読んだ行を後で確実に更新したい | SELECT ... FOR UPDATE |
| バッチを同時に1つだけ動かしたい | advisory lock |
| 複数サービス・複数DBをまたいで排他したい | 分散ロックやキューを検討 |
在庫をSELECT
アプリ側で stock > 0 を確認
UPDATEで在庫を減らす
同時実行されると、複数リクエストが同じ古い値を見てしまいます。
アプリの実装が正しくても、将来の変更や管理画面、手動SQL、バッチ処理で不正データが入る可能性があります。
重要な不変条件はDB制約にします。
トランザクション開始
行ロック取得
外部API呼び出し
DB更新
コミット
外部APIが遅いと、その間ずっと他の処理が待たされます。
ロックを取る範囲はできるだけ短くします。
「正しさ」と「速さ」の両方を確認することが重要です。
レースコンディション対策は、
アプリの工夫 + DBの仕組み + 制約の組み合わせが基本です。
特に重要なのは、次の考え方です。