Springの楽観ロックと悲観ロックを完全解説!@VersionとLockModeTypeの使い分け入門
生徒
「データ更新の競合ってどうやって防ぐんですか?Springで何か方法があるんでしょうか?」
先生
「ありますよ。Springでは、@Versionを使った楽観ロックや、LockModeTypeを使った悲観ロックで、同時更新の問題を防ぐことができます。」
生徒
「えーと、楽観ロックと悲観ロックってどう違うんですか?」
先生
「それじゃあ、2つの違いや使い分け方、そしてSpringでの実装手順を一緒に学んでいきましょう!」
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. 楽観ロックの処理フローと失敗時の対応
Spring Bootで楽観ロックを使った場合、以下の流れで動作します:
- エンティティ取得時に
versionを取得 - 更新処理実行時に
versionを比較 - 一致すれば更新、ずれていれば例外をスロー
このような衝突時には、ユーザーに「再読み込みしてください」といった案内を行う設計が推奨されます。
3. 悲観ロックとは?LockModeTypeで排他制御
悲観ロックは、「同時に他の人が更新するかもしれない」という前提で、読み取り時にロックを取得して他のトランザクションを待たせる仕組みです。
Springでは、EntityManagerを使用して明示的にLockModeType.PESSIMISTIC_WRITEを指定することで、悲観ロックを実現できます。
@PersistenceContext
private EntityManager entityManager;
public Product getProductWithLock(Long id) {
return entityManager.find(Product.class, id, LockModeType.PESSIMISTIC_WRITE);
}
このコードでは、商品データを読み込むと同時に書き込みロックがかかり、他のトランザクションはロックが解放されるまで待たされます。
4. 楽観ロックと悲観ロックの違いと使い分け
楽観ロックと悲観ロックは、トランザクションの競合に対する考え方が異なります。下記のような基準で使い分けると良いでしょう。
- 楽観ロック:更新衝突があまり発生しないケース(例:商品詳細の編集)
- 悲観ロック:高頻度に競合が発生するケース(例:在庫管理、予約処理)
楽観ロックは非同期処理やWebアプリ向きで、悲観ロックはリアルタイム性が求められるバッチ処理などで多用されます。
5. Spring Data JPAで悲観ロックを使う方法
Spring Data JPAでも、リポジトリメソッドに@Lockアノテーションを付けることで悲観ロックが使えます。
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdForUpdate(@Param("id") Long id);
このように指定することで、該当のデータに書き込みロックがかかり、同時アクセスの整合性が保たれます。
6. 楽観ロックで発生する例外の種類と対処
@Versionによる楽観ロックでは、以下のような例外が発生する可能性があります。
OptimisticLockException(JPA標準)ObjectOptimisticLockingFailureException(Spring)
これらの例外は、catchしてリトライする実装を行うことでユーザーに優しい設計が可能です。
try {
userService.updateUser(user);
} catch (ObjectOptimisticLockingFailureException e) {
// ログを記録して再読み込みを促すなどの対応
}
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アプリでは頻繁に使うことになるはずですよ。」
生徒
「逆に悲観ロックは、確実に他の更新を防ぎたい時に使うんですよね?」
先生
「その通りです。在庫のように数値が激しく変動する場面や、予約テーブルなどは悲観ロックのほうが安全です。ただしロックによって性能が落ちることもあるので、必要な場面だけうまく使いましょう。」
生徒
「なるほど…。場面に応じて適切なロックを選ぶって、思っていたより奥深いんですね。」
先生
「ええ、ロックの仕組みを理解すると、アプリケーション全体の設計力がぐっと上がりますよ。こうした基本を丁寧に押さえることで、トラブルを未然に防ぐことができます。」