カテゴリ: Spring 更新日: 2026/09/25

Springの楽観ロックと悲観ロックを完全解説!@VersionとLockModeTypeの使い分け入門

楽観/悲観ロックの使い分け:@Version とロックモードの実装手順
楽観/悲観ロックの使い分け:@Version とロックモードの実装手順

先生と生徒の会話形式で理解しよう

生徒

「データ更新の競合ってどうやって防ぐんですか?Springで何か方法があるんでしょうか?」

先生

「ありますよ。Springでは、@Versionを使った楽観ロックや、LockModeTypeを使った悲観ロックで、同時更新の問題を防ぐことができます。」

生徒

「えーと、楽観ロックと悲観ロックってどう違うんですか?」

先生

「それじゃあ、2つの違いや使い分け方、そしてSpringでの実装手順を一緒に学んでいきましょう!」

1. 楽観ロックとは?@Versionを使った競合防止

1. 楽観ロックとは?@Versionを使った競合防止
1. 楽観ロックとは?@Versionを使った競合防止

楽観ロック(オプティミスティック・ロック)とは、データの更新時に「おそらく他の人は同時に更新しないだろう」と前向きに考えて処理を進める仕組みです。データベースのテーブルを長時間占有(ロック)しないため、システムの処理速度を落としにくいのが大きなメリットです。

Spring Data JPAでは、エンティティのフィールドに@Versionアノテーションを付与するだけで、この高度な排他制御を自動化できます。具体的には、データ取得時のバージョン番号と更新実行時のバージョン番号を比較し、もし数値が異なれば「誰かが先に更新した」と判断してエラーを発生させます。

未経験者向けのイメージ例:ホテルの予約

あなたがWebサイトでホテルの空室を「1部屋」確認したとします。申し込みボタンを押す直前に、別の人がその最後の1部屋を予約してしまいました。このとき、古い情報のまま予約を完了させず、「情報が更新されました。もう一度最初からやり直してください」と安全に止めてくれるのが楽観ロックの役割です。


@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private int stock;

    // このアノテーションが重要!
    @Version
    private Long version;

    // getter/setterは省略
}

上記のコードのように、versionという項目を追加して@Versionを付けるだけです。これにより、データが更新されるたびに数値が 1, 2, 3... と自動でカウントアップされます。

もし同時に二人が同じデータを編集しようとしても、後から保存ボタンを押した人には例外(OptimisticLockingFailureException)が投げられ、不正な上書き(いわゆる「ロストアップデート」)を未然に防ぐことができます。ユーザーには「他のユーザーが更新したため、再度読み込み直してください」といった案内を出すのが一般的です。

2. 楽観ロックの処理フローと失敗時の対応

2. 楽観ロックの処理フローと失敗時の対応
2. 楽観ロックの処理フローと失敗時の対応

Spring Bootで楽観ロックがどのように動作するのか、その仕組みを具体的かつシンプルに紐解いていきましょう。基本的には、データの「バージョン番号」をチェックすることで、後から割り込んできた更新を防ぐ仕組みです。

処理の3ステップ
  1. 取得:DBからデータを読み込む際、現在のバージョン番号(例:1)も一緒に取得します。
  2. 変更:プログラム内でデータを書き換えます。この間、DBのデータはロックされません。
  3. 検証:保存の直前、DBの番号が「1」のままか確認します。1であれば「2」に更新して保存完了。もし誰かが先に更新して「2」になっていれば、エラーを投げて更新を中止します。

プログラミングに慣れていない方のために、身近な「共有ドキュメントの編集」を例にしたサンプルプログラムを見てみましょう。


// サービス層での処理イメージ
@Service
public class DocumentService {
    
    @Transactional
    public void updateContent(Long id, String newContent) {
        // 1. 編集前のデータを取得(この時、内部でversion番号も保持される)
        Document doc = repository.findById(id).orElseThrow();
        
        // 2. 内容を書き換える
        doc.setContent(newContent);
        
        // 3. 保存実行(Springが自動でDBのversionと比較し、不一致なら例外を出す)
        repository.save(doc);
    }
}

もし、あなたが「保存ボタン」を押す数秒前に、同僚が同じドキュメントを更新していたらどうなるでしょうか?その場合、SpringはOptimisticLockingFailureExceptionという例外を発生させます。

このエラーは「システム故障」ではなく、データの整合性を守ったという「安全装置の作動」です。開発者はこの例外をキャッチし、ユーザーに「他の人が更新しました。最新の状態を確認してから、もう一度入力してください」といったメッセージを表示する親切な設計を行うことが重要です。

3. 悲観ロックとは?LockModeTypeで排他制御

3. 悲観ロックとは?LockModeTypeで排他制御
3. 悲観ロックとは?LockModeTypeで排他制御

悲観ロック(ペシミスティック・ロック)とは、「自分がデータを操作している間は、他の誰にも触らせない」という、文字通り慎重で「悲観的」な考え方に基づく仕組みです。データベースの行を物理的に占有するため、データの整合性を最も強力に守ることができます。

Spring Data JPAでは、EntityManagerやアノテーションを通じて、データベースに対して「SELECT ... FOR UPDATE」というSQLを発行し、処理が終わるまで他のユーザーを待機状態にさせることが可能です。これにより、在庫の奪い合いのような、1ミリ秒の差が問題になる処理でも正確にデータを更新できます。

未経験者向けのイメージ例:銀行の窓口

あなたが銀行の窓口で手続きをしている間、その窓口(データ)はあなた専用になり、他の人は後ろに並んで待たなければなりません。手続きが終わるまで他の人は一切手を出せない状態。これが悲観ロックのイメージです。

Javaのプログラムで、特定のデータを取得する際に「書き込み禁止」のロックをかける例を見てみましょう。


@Repository
public class ProductRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public Product getProductWithLock(Long id) {
        // LockModeType.PESSIMISTIC_WRITE を指定することで、
        // DBから取得した瞬間に「他の人は更新不可」のロックをかけます
        return entityManager.find(Product.class, id, LockModeType.PESSIMISTIC_WRITE);
    }
}

このコードを実行すると、指定したIDの商品データを読み込むと同時に、データベース上で「書き込みロック」がかかります。トランザクションが完了(コミットまたはロールバック)するまで、他のプログラムが同じデータを更新しようとしても、順番待ち(待機)の状態になります。

楽観ロックのように「エラーを投げてやり直させる」のではなく、「順番が来るまで待たせる」ことで、確実に処理を完遂させたい決済処理や在庫管理などの重要な場面で活用されます。

4. 楽観ロックと悲観ロックの違いと使い分け

4. 楽観ロックと悲観ロックの違いと使い分け
4. 楽観ロックと悲観ロックの違いと使い分け

楽観ロックと悲観ロックは、トランザクションの競合に対する考え方が異なります。下記のような基準で使い分けると良いでしょう。

  • 楽観ロック:更新衝突があまり発生しないケース(例:商品詳細の編集)
  • 悲観ロック:高頻度に競合が発生するケース(例:在庫管理、予約処理)

楽観ロックは非同期処理やWebアプリ向きで、悲観ロックはリアルタイム性が求められるバッチ処理などで多用されます。

5. Spring Data JPAで悲観ロックを使う方法

5. Spring Data JPAで悲観ロックを使う方法
5. Spring Data JPAで悲観ロックを使う方法

Spring Data JPAにおいて悲観ロックを実装するのは非常に簡単です。リポジトリのメソッドに対して@Lockアノテーションを付与するだけで、複雑なSQLを手書きすることなく、データベースの行ロックを制御できます。

例えば、ECサイトの在庫更新のように「絶対に同時に二人が購入できてはいけない」場面での実装例を見てみましょう。未経験の方でも、この1行のアノテーションが「データの横取りを防ぐシャッター」のような役割を果たすと考えると分かりやすいでしょう。


@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    // @Lockを付けるだけで、DBに対して「SELECT ... FOR UPDATE」が発行されます
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdForUpdate(@Param("id") Long id);
}
ここがポイント!

LockModeType.PESSIMISTIC_WRITEを指定すると、あるユーザーがデータを読み込んだ瞬間から、その処理が終わるまで他のユーザーは同じデータの「読み込み(ロックを伴うもの)」や「更新」ができなくなります。 これにより、在庫のマイナス決済や、二重振り込みといった致命的な不整合を確実に防ぐことができます。

また、ロックの待ち時間を設定したい場合は、@QueryHintsを組み合わせて「何秒待ってもロックが解けないなら諦める(タイムアウト)」といった、より実戦的で安全な設計も可能になります。

6. 楽観ロックで発生する例外の種類と対処

6. 楽観ロックで発生する例外の種類と対処
6. 楽観ロックで発生する例外の種類と対処

Spring Data JPAで@Versionを利用している際、複数のユーザーが同時に同じデータを更新しようとすると、データの整合性を守るために「例外」が発生します。これはプログラムが壊れたわけではなく、古いデータで上書きされないようにSpringが正しくガードした証拠です。

発生する主な例外

一般的には、JPA標準のOptimisticLockExceptionをSpringが使いやすくラップしたObjectOptimisticLockingFailureExceptionがよく投げられます。実務ではこのSpring側の例外をキャッチして処理を記述します。

未経験の方でもイメージしやすいように、「早い者勝ちの注文システム」でエラーが起きた場合の処理例を見てみましょう。エラーを単に表示するだけでなく、裏側で「もう一度最新情報を取ってやり直す」という工夫(リトライ)をすることで、ユーザーの手間を減らすことができます。


@Service
public class OrderService {

    @Autowired
    private ProductRepository repository;

    public void purchaseProduct(Long id) {
        try {
            // 1. 商品を取得(この時のバージョンを記憶)
            Product product = repository.findById(id).orElseThrow();
            
            // 2. 在庫を減らす処理
            product.setStock(product.getStock() - 1);
            
            // 3. 保存(もし他人が先に更新していたらここで例外が発生!)
            repository.save(product);
            
        } catch (ObjectOptimisticLockingFailureException e) {
            // 他の人が先に更新した場合のリカバリー処理
            System.out.println("他のユーザーが注文を完了したため、最新の在庫状況を再取得します。");
            
            // ここで再度メソッドを呼び出したり、ユーザーに「もう一度ボタンを押してください」と表示します
        }
    }
}

ユーザーに優しい設計のコツ

例外が発生したときは、ただ「エラーです」と突き放すのではなく、「他の人が更新したため、画面をリフレッシュしてください」と具体的に案内を出す、あるいはシステム側で数回自動的にリトライを実行させるのが、使いやすいシステムを作るための重要なポイントです。

7. 楽観ロックと悲観ロックの注意点とベストプラクティス

7. 楽観ロックと悲観ロックの注意点とベストプラクティス
7. 楽観ロックと悲観ロックの注意点とベストプラクティス

ロックは強力な仕組みですが、使い方を誤るとパフォーマンスの低下やデッドロックの原因になります。

  • 必要な範囲にだけロックを使う
  • リトライ設計を取り入れる
  • デッドロックの可能性を避けるためロック順を統一する
  • 悲観ロック使用時はtimeout設定を忘れずに

特に高トラフィックなWebサービスでは、楽観ロックを基本に設計し、必要な場面だけ悲観ロックを使うのが一般的なベストプラクティスです。

まとめ

まとめ
まとめ

本記事では、Springにおける楽観ロックと悲観ロックの基本概念から、@Version の具体的な使い方、LockModeType を活用した排他制御の手法、さらに例外発生時のリトライ処理や実運用での注意点までを体系的に整理してきました。楽観ロックはデータ更新の衝突が少ない環境で真価を発揮し、Webアプリケーションのように多数のユーザーが並行してデータを扱う場合でも軽量に整合性を保つことができます。一方で、悲観ロックは確実にデータの一貫性を担保したい金融システムや在庫数の更新のようなケースに向いており、トランザクション管理の正確さが求められる場面で力を発揮します。 実装面では、Spring Data JPA が提供する @Version によるバージョン管理や、EntityManager・@Lock アノテーションを通じたロックモード指定など、開発者が意識すべきポイントは多岐にわたります。特に、実運用ではロックの種類を適切に選び、不要な競合やデッドロックを避けながらアプリケーション全体のパフォーマンスと安全性を両立させる設計が欠かせません。 また、ロックの仕組みを理解すると、更新時の例外処理やユーザーへの案内方法、再試行ロジックの導入といった、より実践的な開発にも活かすことができ、日々の業務に直結する知識となるでしょう。ここではより具体的なコード例を交えながら、まとめとしてもう一度整理していきます。

■ ロックを使った整合性確保の基本コード例

下記は、楽観ロックを用いた更新処理のサンプルです。エンティティのバージョンチェックによって、競合があった場合には例外が発生し、整合性を保つ動作が自然に組み込まれています。


@Transactional
public void updateStock(Long id, int newStock) {
    Product product = productRepository.findById(id)
                                      .orElseThrow(() -> new IllegalArgumentException("not found"));

    product.setStock(newStock);

    try {
        productRepository.save(product);
    } catch (ObjectOptimisticLockingFailureException e) {
        // 再読み込みやリトライなどの設計が必要
        throw new RuntimeException("在庫が他のユーザーに更新されました。もう一度お試しください。");
    }
}

悲観ロックを使う場面では、同時に更新される可能性が高い値に対して書き込みロックを取得することで、安全に排他制御を行うことができます。デッドロックや性能劣化に注意しつつ、必要な箇所にのみ適用することが重要です。


@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdForUpdate(@Param("id") Long id);

このように、楽観ロック・悲観ロックの両方を状況に合わせて使い分けることで、Springアプリケーションの安全性とパフォーマンスを高いレベルで両立させることができます。柔軟に適切なロック手法を選択すれば、データ更新の整合性はもちろん、ユーザー体験の向上にもつながります。

先生と生徒の振り返り会話

生徒

「先生、今日の内容で、楽観ロックと悲観ロックの違いがだいぶ理解できました!」

先生

「それはよかったですね。特に、楽観ロックは軽くて扱いやすいので、普段のWebアプリでは頻繁に使うことになるはずですよ。」

生徒

「逆に悲観ロックは、確実に他の更新を防ぎたい時に使うんですよね?」

先生

「その通りです。在庫のように数値が激しく変動する場面や、予約テーブルなどは悲観ロックのほうが安全です。ただしロックによって性能が落ちることもあるので、必要な場面だけうまく使いましょう。」

生徒

「なるほど…。場面に応じて適切なロックを選ぶって、思っていたより奥深いんですね。」

先生

「ええ、ロックの仕組みを理解すると、アプリケーション全体の設計力がぐっと上がりますよ。こうした基本を丁寧に押さえることで、トラブルを未然に防ぐことができます。」

この記事を読んだ人からの質問

この記事を読んだ人からの質問
この記事を読んだ人からの質問

プログラミング初心者からのよくある疑問/質問を解決します

Springで楽観ロックとは何ですか?@Versionはどのような意味ですか?

楽観ロックは同時更新されない前提で処理し、更新時にバージョン番号が一致しない場合に例外を発生させて整合性を保つ仕組みです。Springでは@Versionアノテーションを使うことで自動的にバージョンチェックが行われます。

Springの悲観ロックはどんなときに使うべきですか?

悲観ロックは同時に他の人が更新する可能性が高い状況、たとえば在庫数の操作や予約システムなど、確実に排他制御が必要な場面で使用されます。

@Versionを使った楽観ロックはどうやって動作するのですか?

エンティティを取得したときのバージョン番号と、更新時のバージョン番号を比較し、異なっていれば例外をスローして更新をキャンセルします。これにより同時更新によるデータ破損を防ぎます。
生成AIを基礎から学びたい方へ

ChatGPTなどの生成AIをこれから学びたい方に向けて、AIスキルアカデミーの口コミ・評判、料金、無料セミナーの内容を詳しくまとめています。

AIスキルアカデミーの口コミ・評判を詳しく見る
カテゴリの一覧へ
新着記事
New1
Spring
Spring Bootの@RequestParamの使い方を解説!初心者でもわかるGETリクエストのパラメータ取得
更新記事
New2
Java
JavaのLocalDateTime.formatメソッドを完全解説!初心者でもわかる日時フォーマット入門
更新記事
New3
Java
Javaのラムダ式とStreamのfilterを徹底解説!初心者でもわかる条件による絞り込み処理
更新記事
New4
Servlet
JavaのServletExceptionクラスを初心者向けに徹底解説!
更新記事
人気記事
No.1
Java&Spring記事人気No1
Servlet
JavaサーブレットのdoPostメソッドとは?初心者向けにやさしく解説!
No.2
Java&Spring記事人気No2
Servlet
JavaのHttpServletRequestWrapperとgetSessionメソッドの使い方を徹底解説!初心者向けServletのセッション管理入門
No.3
Java&Spring記事人気No3
Servlet
JavaのServletExceptionクラスを初心者向けに徹底解説!
No.4
Java&Spring記事人気No4
Servlet
JavaのHttpServletResponseクラスを初心者向けに徹底解説!
No.5
Java&Spring記事人気No5
Java
JavaのStringクラスとlengthメソッドを完全解説!初心者でも理解できる文字列の長さ取得の使い方
No.6
Java&Spring記事人気No6
Servlet
JavaのHttpSessionAttributeListenerとattributeRemovedメソッドの使い方を徹底解説!初心者でもわかるセッション属性の削除と監視
No.7
Java&Spring記事人気No7
Java
Javaのクラスとインスタンス化、コンストラクタの使い方完全ガイド!初心者でもわかるオブジェクト指向の基礎
No.8
Java&Spring記事人気No8
Servlet
JavaのHttpSessionインターフェースとgetSessionContextメソッドを徹底解説!初心者でもわかるセッション管理の基本