ストーリー
要件の整理
interface BookingSystemRequirements {
functional: {
search: "空き状況の検索";
reserve: "仮予約(一定時間の枠確保)";
confirm: "予約確定(決済完了後)";
cancel: "予約キャンセル";
notification: "予約確認・リマインダー通知";
};
nonFunctional: {
consistency: "二重予約を絶対に防ぐ";
availability: "99.99%";
latency: "空き検索 p99 < 500ms";
concurrency: "同一リソースへの1万同時リクエスト対応";
scalability: "1日100万予約";
};
}
ハイレベル設計
┌────────────────────────────────────────────────────────┐
│ │
│ [フロントエンド] │
│ │ │
│ ▼ │
│ [API Gateway] ──→ [認証サービス] │
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ [検索] [予約サービス] │
│ サービス │ │
│ │ ├─→ [在庫管理サービス] → [在庫DB] │
│ │ ├─→ [決済サービス] │
│ │ └─→ [通知サービス] │
│ │ │
│ └─→ [検索インデックス] (Elasticsearch) │
│ [キャッシュ] (Redis) │
│ │
└────────────────────────────────────────────────────────┘
詳細設計
在庫管理と二重予約防止
// 悲観的ロック: SELECT ... FOR UPDATE
class PessimisticBooking {
async reserve(resourceId: string, userId: string): Promise<BookingResult> {
return this.db.transaction(async (tx) => {
// 1. 行ロックを取得(他のトランザクションはここで待機)
const resource = await tx.query(
'SELECT * FROM inventory WHERE id = $1 FOR UPDATE',
[resourceId]
);
// 2. 在庫チェック
if (resource.available <= 0) {
return { status: 'sold_out' };
}
// 3. 在庫を減らして予約を作成
await tx.query(
'UPDATE inventory SET available = available - 1 WHERE id = $1',
[resourceId]
);
const booking = await tx.query(
'INSERT INTO bookings (resource_id, user_id, status, expires_at) VALUES ($1, $2, $3, $4)',
[resourceId, userId, 'RESERVED', new Date(Date.now() + 15 * 60 * 1000)]
);
return { status: 'reserved', bookingId: booking.id };
});
}
}
// 楽観的ロック: バージョン番号によるCAS
class OptimisticBooking {
async reserve(resourceId: string, userId: string): Promise<BookingResult> {
// 1. 現在の在庫とバージョンを取得
const resource = await this.db.query(
'SELECT available, version FROM inventory WHERE id = $1',
[resourceId]
);
if (resource.available <= 0) {
return { status: 'sold_out' };
}
// 2. バージョンが変わっていなければ更新(CAS操作)
const result = await this.db.query(
'UPDATE inventory SET available = available - 1, version = version + 1 WHERE id = $1 AND version = $2',
[resourceId, resource.version]
);
if (result.rowCount === 0) {
// バージョンが変わっていた → 他のユーザーが先に予約した → リトライ
return this.reserve(resourceId, userId); // リトライ(回数制限付き)
}
return { status: 'reserved' };
}
}
仮予約と有効期限管理
// 仮予約パターン: 一定時間枠を確保し、決済後に確定
class ReservationManager {
private readonly RESERVATION_TTL = 15 * 60 * 1000; // 15分
async createReservation(resourceId: string, userId: string): Promise<Reservation> {
const reservation = await this.pessimisticBooking.reserve(resourceId, userId);
// 期限切れ処理のスケジュール
await this.scheduler.schedule({
type: 'EXPIRE_RESERVATION',
reservationId: reservation.bookingId,
executeAt: new Date(Date.now() + this.RESERVATION_TTL),
});
return reservation;
}
// 期限切れの仮予約を自動解放
async expireReservation(reservationId: string): Promise<void> {
const reservation = await this.db.findById(reservationId);
if (reservation.status === 'RESERVED') {
// 在庫を戻す
await this.db.transaction(async (tx) => {
await tx.query(
'UPDATE inventory SET available = available + 1 WHERE id = $1',
[reservation.resourceId]
);
await tx.query(
'UPDATE bookings SET status = $1 WHERE id = $2',
['EXPIRED', reservationId]
);
});
}
}
// 決済完了後に確定
async confirmReservation(reservationId: string): Promise<void> {
await this.db.query(
'UPDATE bookings SET status = $1 WHERE id = $2 AND status = $3',
['CONFIRMED', reservationId, 'RESERVED']
);
}
}
高負荷時の対策
// チケット販売のような瞬間的な高負荷への対策
class HighDemandBooking {
strategies = {
// 1. キューイング: リクエストをキューに入れて順番に処理
queue: {
description: "仮想待合室パターン",
flow: "ユーザー → 待合室 → 順番が来たら予約ページへ",
benefit: "サーバー負荷を平準化",
},
// 2. 抽選方式: 一定期間受付後にランダム選出
lottery: {
description: "受付期間内の応募者から抽選で当選者を決定",
flow: "受付開始 → 応募 → 締切 → 抽選 → 当選通知",
benefit: "先着順の不公平感を解消",
},
// 3. 段階的開放: 在庫を複数回に分けて放出
stagedRelease: {
description: "全在庫を一度に出さず段階的に公開",
flow: "第1弾(30%) → 第2弾(40%) → 第3弾(30%)",
benefit: "ピーク負荷を分散",
},
};
}
まとめ
| ポイント | 内容 |
|---|---|
| 二重予約防止 | 悲観的ロック or 楽観的ロック(CAS) |
| 仮予約パターン | TTL付きの仮確保 → 決済後に確定 |
| 期限切れ管理 | スケジューラで自動解放、在庫を戻す |
| 高負荷対策 | キューイング、抽選、段階的開放 |
チェックリスト
- 悲観的ロックと楽観的ロックの違いを説明できた
- 仮予約パターンの実装を理解した
- 期限切れ予約の自動解放の仕組みを把握した
- 高負荷時の対策パターンを3つ挙げられた
次のステップへ
次は「IoTプラットフォームの設計」を学びます。数百万デバイスからのデータを収集・処理する大規模IoTシステムを設計しましょう。
推定読了時間: 40分