100人が同時にアクセスする際の購入テーブルと在庫テーブルの適切なトランザクション管理について、詳しく解説いたします。
基本的な考慮事項
1. 同時実行制御の課題
- 在庫の二重減算: 複数ユーザーが同じ商品を同時購入する際の競合状態
- データ不整合: 購入処理中に在庫数が変更される問題
- デッドロック: 複数テーブルへの同時アクセスによるロック待ち
2. 適切な分離レベルの選択
READ COMMITTED(推奨)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
利点:
- ダーティリードを防止
- パフォーマンスと整合性のバランスが良い
- PostgreSQL、MySQL、SQL Serverのデフォルト
REPEATABLE READ(高整合性が必要な場合)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
利点:
- ファントムリード以外の問題を防止
- より強い整合性保証
実装パターン
パターン1: 悲観的ロック(FOR UPDATE)
BEGIN TRANSACTION;
-- 在庫テーブルをロック
SELECT stock_quantity
FROM inventory
WHERE product_id = ?
FOR UPDATE;
-- 在庫チェック
IF stock_quantity >= purchase_quantity THEN
-- 購入テーブルに挿入
INSERT INTO purchases (user_id, product_id, quantity, purchase_date)
VALUES (?, ?, ?, NOW());
-- 在庫を減算
UPDATE inventory
SET stock_quantity = stock_quantity - ?
WHERE product_id = ?;
COMMIT;
ELSE
ROLLBACK;
-- 在庫不足エラー
END IF;
パターン2: 楽観的ロック(バージョン管理)
-- テーブル設計にversion列を追加
ALTER TABLE inventory ADD COLUMN version INTEGER DEFAULT 1;
BEGIN TRANSACTION;
-- 現在の在庫とバージョンを取得
SELECT stock_quantity, version
FROM inventory
WHERE product_id = ?;
-- 在庫チェック後、バージョンを条件に更新
UPDATE inventory
SET stock_quantity = stock_quantity - ?,
version = version + 1
WHERE product_id = ?
AND version = ?; -- 取得時のバージョンと一致する場合のみ更新
-- 更新された行数をチェック
IF @@ROWCOUNT = 1 THEN
INSERT INTO purchases (user_id, product_id, quantity, purchase_date)
VALUES (?, ?, ?, NOW());
COMMIT;
ELSE
ROLLBACK;
-- 競合発生、リトライ処理
END IF;
パターン3: アトミックな在庫減算
BEGIN TRANSACTION;
-- 在庫減算と同時に制約チェック
UPDATE inventory
SET stock_quantity = stock_quantity - ?
WHERE product_id = ?
AND stock_quantity >= ?; -- 在庫不足の場合は更新されない
-- 更新された行数をチェック
IF @@ROWCOUNT = 1 THEN
INSERT INTO purchases (user_id, product_id, quantity, purchase_date)
VALUES (?, ?, ?, NOW());
COMMIT;
ELSE
ROLLBACK;
-- 在庫不足エラー
END IF;
パフォーマンス最適化
1. インデックス設計
-- 購入テーブル
CREATE INDEX idx_purchases_user_product ON purchases(user_id, product_id);
CREATE INDEX idx_purchases_date ON purchases(purchase_date);
-- 在庫テーブル
CREATE INDEX idx_inventory_product ON inventory(product_id);
2. コネクションプール設定
# 推奨設定例
maximumPoolSize=20
minimumIdle=5
connectionTimeout=30000
idleTimeout=600000
maxLifetime=1800000
3. デッドロック回避
-- 常に同じ順序でテーブルにアクセス
-- 1. inventory テーブル
-- 2. purchases テーブル
-- の順序を維持
具体的な実装例(Java + Spring)
@Service
@Transactional(isolation = Isolation.READ_COMMITTED)
public class PurchaseService {
@Retryable(value = {OptimisticLockingFailureException.class},
maxAttempts = 3, backoff = @Backoff(delay = 100))
public PurchaseResult processPurchase(Long userId, Long productId, Integer quantity) {
// 悲観的ロックで在庫取得
Inventory inventory = inventoryRepository.findByProductIdForUpdate(productId);
if (inventory.getStockQuantity() < quantity) {
throw new InsufficientStockException("在庫不足");
}
// 購入レコード作成
Purchase purchase = new Purchase(userId, productId, quantity, LocalDateTime.now());
purchaseRepository.save(purchase);
// 在庫更新
inventory.decreaseStock(quantity);
inventoryRepository.save(inventory);
return new PurchaseResult(purchase.getId(), "購入完了");
}
}
監視とメトリクス
1. 重要な監視項目
-
デッドロック発生率:
SHOW ENGINE INNODB STATUS
-
ロック待機時間:
innodb_lock_wait_timeout
- トランザクション実行時間: アプリケーションレベルで測定
- 在庫不足エラー率: ビジネスメトリクス
2. アラート設定
-- デッドロック監視クエリ例
SELECT
COUNT(*) as deadlock_count,
DATE(created_time) as date
FROM information_schema.innodb_metrics
WHERE name = 'lock_deadlocks'
GROUP BY DATE(created_time);
本番環境での推奨設定
MySQL/MariaDB
SET GLOBAL innodb_lock_wait_timeout = 5;
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
SET GLOBAL innodb_deadlock_detect = ON;
PostgreSQL
SET default_transaction_isolation = 'read committed';
SET deadlock_timeout = '1s';
SET statement_timeout = '30s';
この設計により、100人の同時アクセスでも安全で効率的な購入処理が実現できます。特に悲観的ロックとREAD COMMITTED分離レベルの組み合わせが、パフォーマンスと整合性のバランスが最も良い選択となります。