PR

排他制御の基本:悲観ロックと楽観ロックを3分でわかりやすく

Database

「排他制御」は主にデータベース関連の処理に関する文脈で使われる用語で、同じデータを複数のユーザーやプロセスが同時に変更しようとしたときに起こる問題を防ぐための仕組みです。

例えば2人が同じドキュメントを同時に編集しようとすると、変更が競合してデータが壊れてしまいますよね。これを避けるために用いられるのが排他制御で、適切な排他制御を導入することで安全なデータベース利用が可能になります。

ザックリいえば、同時に複数の人が同じデータに対して更新をしないようにデータにロックをかけましょう!というのが「排他処理」です。

排他制御は大きく以下の2通りあります。ここではそれぞれどのような仕組みで排他制御を実現しているのか?という観点で1からわかりやすく初心者向けにご説明します。

ロックの種類説明適用場面
楽観ロック (Optimistic Locking)データの競合が少ないと仮定し、データの読み取りと書き込みの間にロックをかけず、更新時に競合が発生した場合にのみ処理をリトライする方法。データの更新が比較的少なく、読み取りが多いシナリオ
悲観ロック (Pessimistic Locking)データの競合が発生する可能性が高いと仮定し、データの読み取り時点でロックをかけて、他のトランザクションがそのデータにアクセスできないようにする方法。競合が頻繁に発生するシナリオや、データの一貫性が非常に重要な場面
スポンサーリンク

楽観ロック (Optimistic Locking) とは?

楽観ロックは、「他の人が同時にデータを使うことは少ないだろう」と考えてデータを扱う方法です。簡単に言うと、楽観ロックはデータの変更が同時に起きることをあまり心配せずに進めるけど、最後に問題がないかチェックする仕組みです。

楽観ロックの動作原理

  1. データの読み取り
    • まず、データを読み取るときにそのデータのバージョンや変更された時間を記録します。これは「データがこの時点でどうなっているか」を覚えておくためです。
  2. データの変更
    • 次に、データを変更します。この時点では他の誰かも同じデータを変更しているかもしれないと考えますが、気にせずに変更を進めます。
  3. データの更新前のチェック
    • データをデータベースに保存する前に、最初に記録したバージョンや変更された時間と、現在のデータのバージョンや時間を比べます。
      • バージョンが同じ→他の誰もその間にデータを変更していないことが確認できるので、変更を保存。
      • バージョンが異なる→他の誰かがそのデータを変更したことが分かるので、今回の変更は失敗とみなします。再度データを読み取り直して、もう一度最初からやり直し。

楽観ロックの具体例

シナリオ オンラインショッピング

背景: オンラインショッピングサイトで商品を購入する場合を考えます。このサイトでは在庫管理のために楽観ロックを使っていることとします。

  1. データの読み取り
    • あなたがサイトにアクセスして、欲しい商品を選択。この時点で在庫情報が10個存在することと現在の時間を記録しておきます。
  2. データの変更
    • 商品をカートに入れ、10個全部の購入手続きを進めます。(クレジットカード情報を入力し配送先の確認などを行うイメージ。)この間、他のユーザーも同じ商品を購入しようとしていますが、あなたは特に気にせず手続きを進めます。
  3. データの更新前のチェック
    • 最後に「購入」ボタンを押したときにシステムで再度在庫を確認します。
      • 在庫がまだ10個ある場合→システムは他の誰もこの商品を購入していないことを確認。これにより、あなたの購入手続きを完了し、「購入が成功しました」と表示。
      • 在庫が変わっている場合(例えば、8個に減っている場合)→システムは、他の誰かがその間に商品を購入したことを検出します。この場合、システムは「在庫が不足しています」と表示し、あなたの購入手続きをやり直すように求めます。

このように楽観ロックは、データの競合が少ないことを前提として処理を進め、最後に一貫性を確認する方法です。システムはデータの更新時にだけ問題をチェックするので、全体的なパフォーマンスが向上しますが、競合が発生した場合には再試行が必要となるデメリットがあります。

悲観ロック (Pessimistic Locking) とは?

悲観ロックは、「他の人が同時にデータを使う可能性が高いから、今使っている間は他の人が触らないようにしよう」と考えてデータを扱う方法です。簡単に言うと、悲観ロックはデータを使うときにしっかりと鍵をかけて、誰もそのデータに触れないようにする仕組みです。

悲観ロックの動作原理

  1. データの読み取り時にロックをかける
    • データを読み取るときに、そのデータに「鍵」をかけます。これにより、他の人はそのデータを使えなくなります。
  2. データの変更
    • 鍵をかけた状態でデータを変更。誰もそのデータにアクセスできないので、安心して変更可能。
  3. トランザクションの終了
    • 変更が終わり、トランザクションが完了したら鍵を外す。→これで他の人が再びそのデータを使えるようになる。

参考 トランザクションとは?

悲観ロックの具体例

シナリオ オンラインデータベースのレコード編集

  1. データの読み取り時にロックをかける
    • 会社のデータベースで顧客情報を編集しようとする場合に悲観ロックを採用しているとします。この場合顧客情報を読み取ると同時にその情報にロックをかけます。これにより、他の社員はその顧客情報を編集できなくなります。
  2. データの変更
    • 顧客情報を更新します(例えば、住所を変更したり連絡先を追加したり)。この間、他の社員はその顧客情報を編集しようとすると、「他の人が編集中です」と表示され、待つことになります。
  3. トランザクションの終了
    • あなたが編集を終えて変更を保存します。すると、ロックが解除され、他の社員もその顧客情報を編集できるようになります。

このように悲観ロックはデータを使っている間に他の人がそのデータにアクセスできないようにする方法です。データの競合が頻繁に発生する場合や、データの一貫性が非常に重要な場合に使われます。

まとめ 悲観ロックと楽観ロックの違い

ロックの種類説明適用場面
楽観ロック (Optimistic Locking)データの競合が少ないと仮定し、データの読み取りと書き込みの間にロックをかけず、更新時に競合が発生した場合にのみ処理をリトライする方法。データの更新が比較的少なく、読み取りが多いシナリオ
悲観ロック (Pessimistic Locking)データの競合が発生する可能性が高いと仮定し、データの読み取り時点でロックをかけて、他のトランザクションがそのデータにアクセスできないようにする方法。競合が頻繁に発生するシナリオや、データの一貫性が非常に重要な場面

悲観ロックと楽観ロックの実装例

ここからは「楽観ロック」と「悲観ロック」について再度振り返りつつ、より具体的にその実装例をお示します。

楽観ロック(Optimistic Lock)

概要
楽観ロックは、「データが他のトランザクションによって変更される可能性は低い」と仮定し、ロックをかけずに更新を試みます。競合が発生した場合のみエラーを出して処理をやり直します。

メリット

  • ロックを使用しないため、パフォーマンスに優れる
  • 競合が少ない環境では効率的に動作

デメリット

  • 競合が頻発すると、更新のリトライが発生し、処理が遅くなる
  • 競合時に例外処理を適切に実装する必要がある

SQLを使った楽観ロックの例

UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 3;

このSQLでは、version 列を利用して競合チェックを行います。もし version = 3 のレコードが他のトランザクションで変更されていた場合、この UPDATE は影響を与えず、更新されません(つまり競合が発生したことを示す)。

Java(JPA/Hibernate)を使った楽観ロックの例

import jakarta.persistence.*;

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private int stock;
    
    @Version // 楽観ロックのためのバージョン管理
    private int version;

    // getter, setter
}

public void updateStock(EntityManager entityManager, Long productId) {
    entityManager.getTransaction().begin();
    
    Product product = entityManager.find(Product.class, productId);
    product.setStock(product.getStock() - 1);
    
    entityManager.getTransaction().commit();
}

ここでは @Version アノテーションを使い、バージョン管理を行っています。
もし commit() する前に他のトランザクションが Product を更新していた場合、OptimisticLockException がスローされ、更新が失敗します。

例外処理の実装例

public void updateStockWithRetry(EntityManager entityManager, Long productId) {
    int retryCount = 3;

    for (int i = 0; i < retryCount; i++) {
        try {
            entityManager.getTransaction().begin();
            Product product = entityManager.find(Product.class, productId);
            product.setStock(product.getStock() - 1);
            entityManager.getTransaction().commit();
            return;
        } catch (OptimisticLockException e) {
            entityManager.getTransaction().rollback();
            System.out.println("競合発生。リトライ " + (i + 1));
        }
    }
    throw new RuntimeException("更新に失敗しました");
}

このように OptimisticLockException をキャッチして、一定回数リトライすることで、楽観ロックによる競合が発生した際の対処を行います。

悲観ロック(Pessimistic Lock)

概要
悲観ロックは、「データが他のトランザクションによって変更される可能性がある」と考え、最初にロックを取得する方式です。更新対象のデータに排他ロックをかけ、他のプロセスが更新できないようにします。

メリット

  • データ競合が頻発する場合に適している
  • データの一貫性を高く維持できる

デメリット

  • ロックの保持時間が長くなるとパフォーマンスに悪影響
  • デッドロックのリスクがある

SQLを使った悲観ロックの例(MySQL)

START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE; -- レコードに排他ロック
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

このSQLでは FOR UPDATE を使用して、id = 1 のレコードに排他ロックをかけています。他のトランザクションがこのレコードを更新しようとすると、現在のトランザクションが終了するまで待たされます。

Java(JPA/Hibernate)を使った悲観ロックの例

import jakarta.persistence.*;

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private int stock;

    // getter, setter
}

public void updateStock(EntityManager entityManager, Long productId) {
    entityManager.getTransaction().begin();
    
    Product product = entityManager.find(Product.class, productId, LockModeType.PESSIMISTIC_WRITE);
    product.setStock(product.getStock() - 1);
    
    entityManager.getTransaction().commit();
}

LockModeType.PESSIMISTIC_WRITE を指定すると、対象のエンティティに対して排他ロックが取得されます。これにより、他のトランザクションが同じ Product を更新しようとするとブロックされます。

悲観ロックと楽観ロック:どちらを選ぶべきか?

比較項目悲観ロック楽観ロック
競合の頻度高頻度な競合に強い低頻度な競合に向いている
パフォーマンス低め(ロックによる待機)高め(ロック不要)
データの一貫性高い(競合なし)競合時はリトライが必要
実装の容易さシンプル(ロックを取るだけ)競合処理の実装が必要
  • 悲観ロックが適しているケース
    • 銀行の口座取引など、データの厳密な一貫性が求められる場面
    • 更新競合が頻繁に発生し、リトライ処理が過負荷になる場合
  • 楽観ロックが適しているケース
    • 競合がほぼ発生しないシステム(ユーザーごとの個別設定データなど)
    • 大量のトランザクションが発生するが、競合が少ない場合
  • 悲観ロック は「競合することを前提」に排他制御を行い、確実なデータ更新を保証するが、パフォーマンスの問題がある。
  • 楽観ロック は「競合しないことを前提」に高速に処理できるが、競合が起こるとリトライが必要。

上記の実装方法を理解し、システムの特性に応じて適切なロック方式を選択することが重要です。

タイトルとURLをコピーしました