レースコンディション
複数のプロセスやスレッドが共有リソースに同時アクセスし、実行順序によって結果が変わる不具合
並行処理品質
レースコンディションとは
レースコンディション (Race Condition) は、複数のプロセスやスレッドが共有リソース (変数、ファイル、DB レコード) に同時にアクセスし、実行順序によって結果が予期しない値になる不具合である。再現が困難で、テストで検出しにくい厄介なバグだ。
典型的な例: 在庫の二重引き当て
在庫: 1個
リクエスト A: 在庫を確認 → 1個ある → 購入処理
リクエスト B: 在庫を確認 → 1個ある → 購入処理 ← A の更新前に読んだ!
結果: 在庫 1 個なのに 2 人が購入 → 在庫が -1 に
「確認」と「更新」の間に別のリクエストが割り込むことで発生する。これを TOCTOU (Time of Check to Time of Use) 問題と呼ぶ。
DynamoDB での対策: 条件付き書き込み
// ❌ レースコンディション: 読み取りと書き込みの間に割り込まれる
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 - 1',
}));
}
// ✅ 条件付き書き込み: アトミックに確認と更新を実行
await ddb.send(new UpdateCommand({
TableName: 'Products',
Key: { id: 'prod-1' },
UpdateExpression: 'SET stock = stock - 1',
ConditionExpression: 'stock > :zero',
ExpressionAttributeValues: { ':zero': 0 },
}));
// stock が 0 以下なら 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 + 1) - 分散ロック (DynamoDB ベースのロック)
- SQS FIFO キューで直列化
競合状態の対策
| 対策 | 説明 | AWS での実装 |
|---|---|---|
| ミューテックス | 排他制御 | DynamoDB 条件付き書き込み |
| 楽観ロック | バージョン番号で検出 | DynamoDB ConditionExpression |
| アトミック操作 | 不可分な操作 | DynamoDB UpdateExpression |
| キュー | 順序を保証 | SQS FIFO |
現場での応用を知るには関連書籍も役立つ。