レースコンディション

複数のプロセスやスレッドが共有リソースに同時アクセスし、実行順序によって結果が変わる不具合

並行処理品質

レースコンディションとは

レースコンディション (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

現場での応用を知るには関連書籍も役立つ。

関連用語