レースコンディション
複数のプロセスやスレッドが共有リソースに同時アクセスし、実行順序によって結果が変わる不具合
レースコンディションとは
レースコンディション (Race Condition) は、複数のプロセスやスレッドが共有リソース (変数、ファイル、DB レコード) に同時にアクセスし、実行順序によって結果が予期しない値になる不具合である。再現が困難で、テストで検出しにくい厄介なバグだ。
典型的な例: 在庫の二重引き当て
在庫が 1 個しか残っていない商品に、購入リクエストが 2 つほぼ同時に届いた場面を追ってみる。どちらの処理も「在庫を読む」「在庫を減らす」の 2 段階に分かれており、この 2 段階の隙間に相手の更新が入り込む。
在庫: 1個
リクエスト A: 在庫を確認 → 1個ある → 購入処理
リクエスト B: 在庫を確認 → 1個ある → 購入処理 ← A の更新前に読んだ!
結果: 在庫 1 個なのに 2 人が購入 → 在庫が -1 に
「確認」と「更新」の間に別のリクエストが割り込むことで発生する。これを TOCTOU (Time of Check to Time of Use) 問題と呼ぶ。
割り込みが起こり得る時間は確認から更新までの区間だけなので、処理が速ければ窓は狭くなるが、ゼロにはならない。アクセスが少ないうちはたまたま重ならず正常に見え、同時アクセスが増えた途端に頻発する。手元で 1 リクエストずつ試す限り再現しないのは、テストが窓を踏まないからであり、バグが無いからではない。
DynamoDB での対策: 条件付き書き込み
読み取りと書き込みを別々の API 呼び出しに分けている限り、その間の割り込みは防げない。DynamoDB では、更新してよい条件を更新リクエスト自体に同梱することで、確認と更新を 1 回の操作にまとめられる。
// ❌ レースコンディション: 読み取りと書き込みの間に割り込まれる
const item = await ddb.send(new GetCommand({ TableName: 'Products', Key: { id: 'prod-1' } }));
if (item.Item!.stock > 0) {
await ddb.send(new UpdateCommand({
TableName: 'Products',
Key: { id: 'prod-1' },
UpdateExpression: 'SET stock = stock - :dec',
ExpressionAttributeValues: { ':dec': 1 },
}));
}
// ✅ 条件付き書き込み: アトミックに確認と更新を実行
await ddb.send(new UpdateCommand({
TableName: 'Products',
Key: { id: 'prod-1' },
UpdateExpression: 'SET stock = stock - :dec',
ConditionExpression: 'stock > :zero',
ExpressionAttributeValues: { ':dec': 1, ':zero': 0 },
}));
// stock が 0 以下なら ConditionalCheckFailedException がスロー
更新式の中に数値をそのまま書くことはできず、減らす量も :dec のような式属性値として渡す。また、条件を満たさなかったときは例外が返るだけで在庫は動かないため、呼び出し側で ConditionalCheckFailedException を捕まえて「売り切れ」として扱う分岐が必要になる。捕まえ忘れると、正常な在庫切れがサーバーエラーとしてユーザーに見えてしまう。
RDB での対策
楽観的ロック (バージョン番号)
-- 読み取り時にバージョンを取得
SELECT stock, version FROM products WHERE id = 'prod-1';
-- stock: 5, version: 3
-- 更新時にバージョンを条件に含める
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 'prod-1' AND version = 3;
-- 他のトランザクションが先に更新していたら、version が 4 になっており、0 行更新
悲観的ロック (SELECT FOR UPDATE)
BEGIN;
SELECT stock FROM products WHERE id = 'prod-1' FOR UPDATE; -- 行ロック
UPDATE products SET stock = stock - 1 WHERE id = 'prod-1';
COMMIT;
Lambda でのレースコンディション
Lambda は同時に複数のインスタンスが実行されるため、共有リソース (DynamoDB、S3) へのアクセスでレースコンディションが発生しやすい。
対策:
- DynamoDB の条件付き書き込み (ConditionExpression)
- DynamoDB のアトミックカウンター (
SET #count = #count + :inc。COUNTは予約語なのでExpressionAttributeNamesで別名を付ける) - 分散ロック (DynamoDB ベースのロック)
- SQS FIFO キューで直列化 (順序が保証されるのは同じメッセージグループ ID の中だけなので、直列にしたい単位をグループ ID に選ぶ)
競合状態の対策
どの対策も「割り込みが入らない単位を作る」ことに行き着くが、単位の作り方が違う。ロックを取って他を待たせるか、衝突を後から検出して弾くか、確認と更新を 1 回の操作にまとめるか、順番に 1 つずつ処理するかの 4 通りである。
| 対策 | 説明 | AWS での実装 |
|---|---|---|
| ミューテックス | ロックを取った 1 つだけが進む | DynamoDB ベースの分散ロック (有効期限付き) |
| 楽観ロック | バージョン番号で検出 | DynamoDB ConditionExpression |
| アトミック操作 | 確認と更新を 1 回の操作にまとめる | DynamoDB の更新式による加減算 |
| キュー | 直列に 1 つずつ処理 | SQS FIFO (メッセージグループ ID 単位) |
衝突がまれなら、楽観ロックやアトミック操作で待ち時間を作らずに済む。衝突が多い場所では再試行が空回りして処理量が落ちるため、ロックや直列化に寄せる。分散ロックを選ぶときは、ロックを持ったまま処理が落ちても解放されるよう有効期限を必ず付ける。期限の無いロックは 1 度の異常終了で在庫が永久に止まる。
現場での応用を知るには関連書籍も役立つ。
この記事は役に立ちましたか?
関連用語
並行処理 (Concurrency) とは - 並列処理 (Parallelism) との違い
Concurrency (並行処理) の意味を解説。複数のタスクを論理的に同時進行させる手法で、物理的に同時実行する並列処理 (Parallelism) とは区別される。Node.js / Go / Rust の並行処理モデルの違いも紹介。
楽観的ロック
データの読み取り時にロックせず、更新時にバージョンを検証して競合を検出する排他制御
分散ロック
分散システムで複数のプロセスが同じリソースに同時アクセスすることを防ぐ排他制御
ミューテックス
複数のスレッドが共有リソースに同時アクセスすることを防ぐ排他制御の同期プリミティブ
ライトアンプリフィケーション
データベースへの 1 回の論理的な書き込みが、内部的に複数回の物理的な書き込みを引き起こす現象
Terraform State
Terraform がインフラの現在の状態を管理するファイルとリモートバックエンド
関連する記事
スマホ / パソコン入門書の選び方 - シニアの家族に贈る 1 冊の見つけ方
スマホとパソコンの入門書を「贈る側 / 教える側」の視点で選ぶガイド。機種と OS の一致、文字の大きさ、年度の確認という 3 条件と、Android / iPhone / Windows 11 それぞれの定番書、安全対策の 1 冊まで 2026 年 8 月時点の収載書から紹介します。
技術書の情報が古くなったときの対処法
技術書の内容が古くなったときの対処法を紹介。古くなる部分と古くならない部分の見分け方、購入前の鮮度チェック方法を解説します。
技術書の中古本 / 古本の賢い買い方 - 安く手に入れて賢く学ぶ
技術書を中古で買うときの判断基準と注意点を解説します。中古で買っていい本と新品で買うべき本の見分け方、状態チェックのポイントを紹介。