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?

PostgreSQL の FOR UPDATE による行ロックを Node.js + Prisma で理解する

Posted at

はじめに

データベースの同時実行制御において、FOR UPDATE は重要な機能です。本記事では、PostgreSQL の FOR UPDATE による行ロックの挙動を、Node.js + Prisma を使った実際のコードで確認します。

FOR UPDATE とは

FOR UPDATESELECT 文に付与することで、取得した行に対して排他ロック(行ロック)を取得します。

SELECT * FROM m_user WHERE user_id = 1 FOR UPDATE;

このロックには以下の特徴があります:

  • 排他的: 他のトランザクションが同じ行に対して FOR UPDATE を実行しようとすると、ロックが解放されるまで待機する
  • トランザクション終了まで保持: COMMIT または ROLLBACK でロックが解放される
  • 行レベル: テーブル全体ではなく、条件に一致した行のみがロックされる

ユースケース

  • 残高の更新(二重引き落とし防止)
  • 在庫の減算(売り過ぎ防止)
  • 予約システム(ダブルブッキング防止)

検証コード

2つのトランザクションを並行実行し、FOR UPDATE の挙動を確認します。

import { PrismaClient } from '@prisma/client';

async function main() {
  const prisma1 = new PrismaClient();
  const prisma2 = new PrismaClient();

  const userId = 1;

  // トランザクション1: 先にロックを取得
  const transaction1 = async () => {
    console.log('[TX1] 開始');
    await prisma1.$transaction(
      async (tx) => {
        console.log('[TX1] FOR UPDATE でロック取得');
        await tx.$queryRaw`
          SELECT * FROM m_user
          WHERE user_id = ${userId}
          FOR UPDATE
        `;

        console.log('[TX1] ロック取得完了。5秒待機...');
        await new Promise((resolve) => setTimeout(resolve, 5000));

        console.log('[TX1] UPDATE 実行');
        await tx.user.update({
          where: { userId },
          data: { firstName: '先にロック取得' },
        });

        console.log('[TX1] COMMIT');
      },
      { timeout: 30000 },
    );
    console.log('[TX1] 完了\n');
  };

  // トランザクション2: 少し遅れて開始(ロック待ちになる)
  const transaction2 = async () => {
    await new Promise((resolve) => setTimeout(resolve, 1000));

    console.log('[TX2] 開始');
    await prisma2.$transaction(
      async (tx) => {
        console.log('[TX2] FOR UPDATE でロック取得を試みる...');
        await tx.$queryRaw`
          SELECT * FROM m_user
          WHERE user_id = ${userId}
          FOR UPDATE
        `;

        console.log('[TX2] ロック取得完了(TX1のCOMMIT後)');

        console.log('[TX2] UPDATE 実行');
        await tx.user.update({
          where: { userId },
          data: { firstName: '後からロック取得' },
        });

        console.log('[TX2] COMMIT');
      },
      { timeout: 30000 },
    );
    console.log('[TX2] 完了\n');
  };

  // 並行実行
  await Promise.all([transaction1(), transaction2()]);

  // 最終結果を確認
  const result = await prisma1.user.findUnique({
    where: { userId },
    select: { userId: true, firstName: true },
  });
  console.log('=== 最終結果 ===');
  console.log(result);
}

main();

ポイント

  • 2つの PrismaClient インスタンス: 別々のデータベース接続を模倣するため
  • TX1 は 5 秒待機: ロック保持中に TX2 がロック取得を試みる状況を作る
  • TX2 は 1 秒遅れで開始: TX1 がロックを取得した後に開始させる

実行結果

[TX1] 開始
[TX1] FOR UPDATE でロック取得
[TX1] ロック取得完了。5秒待機...
[TX2] 開始
[TX2] FOR UPDATE でロック取得を試みる...
      ← ここで TX2 はブロックされる(約4秒間)
[TX1] UPDATE 実行
[TX1] COMMIT
[TX1] 完了

[TX2] ロック取得完了(TX1のCOMMIT後)
[TX2] UPDATE 実行
[TX2] COMMIT
[TX2] 完了

=== 最終結果 ===
{ userId: 1, firstName: '後からロック取得' }

実行結果から分かること

時点 TX1 TX2
0秒 ロック取得 -
1秒 待機中 ロック取得試行 → ブロック
5秒 UPDATE + COMMIT ブロック中
5秒直後 完了 ロック取得成功
5秒+ - UPDATE + COMMIT

重要な観察点:

  1. TX2 の FOR UPDATE は、TX1 が COMMIT するまでブロックされる
  2. TX1 の COMMIT 直後に TX2 がロックを取得できる
  3. 最終的な値は、後から COMMIT した TX2 の値になる

FOR UPDATE のバリエーション

PostgreSQL では FOR UPDATE にオプションを付けられます:

-- ロック取得できなければ即座にエラー
SELECT * FROM m_user WHERE user_id = 1 FOR UPDATE NOWAIT;

-- 指定時間待ってもロック取得できなければエラー(PostgreSQL 9.3+)
SET lock_timeout = '5s';
SELECT * FROM m_user WHERE user_id = 1 FOR UPDATE;

-- ロック取得できなければスキップ(バッチ処理向け)
SELECT * FROM m_user WHERE user_id = 1 FOR UPDATE SKIP LOCKED;

Prisma での注意点

Prisma で FOR UPDATE を使う場合、$queryRaw を使用する必要があります:

// FOR UPDATE は $queryRaw で実行
await tx.$queryRaw`
  SELECT * FROM m_user WHERE user_id = ${userId} FOR UPDATE
`;

// その後の更新は通常の Prisma API でOK
await tx.user.update({
  where: { userId },
  data: { firstName: '更新値' },
});

まとめ

  • FOR UPDATE は行レベルの排他ロックを取得する
  • ロックは COMMIT / ROLLBACK まで保持される
  • 他のトランザクションは、ロックが解放されるまで待機する
  • 同時実行制御が必要な場面(残高更新、在庫管理など)で活用できる
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?