LLaravel Bootcamp

Day 6責務の分離とデータ整合性 — サービス層・トランザクション・排他制御

排他制御 — 同時アクセスの競合を防ぐ

悲観ロックと楽観ロックの仕組みと使い分け。在庫・残高の競合で理解する。

何が問題なのか — 競合状態

在庫が1個の商品を、2人がほぼ同時に注文したとします。両方の処理が『在庫は1個ある』と読み取り、両方が『在庫を0にして注文を作る』と進むと、在庫1個に対して2件の注文が成立してしまいます。トランザクションだけでは、この『同時に読んで同時に書く』競合は防ぎきれません。

排他制御は、こうした同時アクセスによるデータ競合を防ぐ仕組みです。代表的な方法が (悲観ロック)と (楽観ロック)です。

悲観ロック — 先に読んだ人が鍵をかける

悲観ロックは『競合はよく起きる』と悲観的に考え、対象の行を読む時点でロックをかけ、他の処理を待たせる方式です。Laravelでは「lockForUpdate()」でその行に更新用ロックをかけます。

PHP
DB::transaction(function () use ($productId, $qty) {
    // この行にロックをかける。同じ行を触る他の処理はここで待たされる
    $product = Product::where('id', $productId)
        ->lockForUpdate()
        ->first();

    if ($product->stock < $qty) {
        throw new OutOfStockException();
    }

    $product->decrement('stock', $qty);
    // トランザクションのコミットでロックが解放される
});
lockForUpdate() で在庫行をロックして競合を防ぐ

悲観ロックの注意点

他の処理を待たせるため、ロックを長く保持すると全体の処理が詰まります。またロックの取得順序が処理ごとにばらばらだと、互いに待ち合ってデッドロックになることがあります。ロック区間は短く、順序を揃えるのが原則です。

楽観ロック — 更新時にバージョンで衝突を検知する

楽観ロックは『競合はめったに起きない』と楽観的に考え、ロックはかけません。代わりにテーブルに「version」カラムを持たせ、更新時に『自分が読んだときのバージョンのままか』を条件にします。もし他の人が先に更新してバージョンが変わっていたら、自分の更新は0件になり、衝突を検知できます。

PHP
// 読み込み時に version も取得しておく(例: version = 5)
$product = Product::find($productId);
$currentVersion = $product->version;

// 更新は「読んだときの version と一致する行」だけを対象にする
$updated = Product::where('id', $productId)
    ->where('version', $currentVersion)
    ->update([
        'stock'   => $product->stock - $qty,
        'version' => $currentVersion + 1,
    ]);

if ($updated === 0) {
    // 0件 = 誰かが先に更新して version が変わっていた
    throw new ConflictException('他の操作と競合しました。やり直してください。');
}
version カラムで衝突を検知する楽観ロック

どう使い分けるか

悲観ロックが向く場面

競合が頻繁に起きる/絶対に取りこぼせない場面。例: 残りわずかな在庫の取り合い、口座残高の更新。確実だが待ちが発生する。

楽観ロックが向く場面

競合がまれな場面。例: 管理画面での商品情報の編集、プロフィール更新。ロックしないので速いが、衝突時はやり直しが必要。

なぜ二種類を使い分けるのか

悲観ロックは確実ですが他を待たせるため、競合がまれな場面で使うと無駄に遅くなります。楽観ロックは速いですが、衝突が多い場面ではやり直しが頻発して逆に非効率です。つまり『競合の起きやすさ』でコストの安い方を選ぶ、というトレードオフの判断になります。

二重登録の防止とも関係する

送信ボタンの連打で同じ注文が2件登録される問題も競合の一種です。ユニーク制約や (冪等性)を組み合わせ、『同じリクエストが2回来ても結果は1回分』にする設計と併せて考えると堅牢になります。

面接ではこう言う

「在庫や残高のように同時更新で競合する場面では排他制御を使います。競合が頻繁なら悲観ロックでlockForUpdate、まれなら楽観ロックでversionカラムを使う、と競合の起きやすさで選びます。悲観ロックは確実だが待ちが出る、楽観ロックは速いが衝突時にやり直す、というトレードオフで判断していると説明します」

読んでも腑に落ちない所は、AIに噛み砕いてもらいましょう。

理解度チェック

答えられれば、面接で説明できる状態に一歩近づきます。

Q1

悲観ロック(lockForUpdate)の特徴として正しいものは?

Q2

楽観ロックで「versionカラム」を使う目的は?

Q3

悲観ロックと楽観ロックをどう使い分けますか。在庫の例で説明してください。

記述式です。まず自分の言葉で答えを考えてから、模範解答を開いて自己採点してください。