0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

springbootによる同時実行対応について

Posted at

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分離レベルの組み合わせが、パフォーマンスと整合性のバランスが最も良い選択となります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?