データベースを利用するWebアプリケーションでは、複数のユーザーやプロセスが同時に同じデータを更新しようとする「同時更新」問題が発生します。Spring JPAのロック戦略は、このような並行アクセス時のデータ整合性を保証するための重要な機能です。本記事では、@Versionアノテーションを活用した楽観的ロック(Optimistic Locking)の仕組みとOptimisticLockExceptionのハンドリング、@LockアノテーションとLockModeTypePESSIMISTIC_READPESSIMISTIC_WRITE)を使った悲観的ロック(Pessimistic Locking)の実装、デッドロック対策、そしてロック戦略の選定基準まで、実務で必要となる知識を体系的に解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Data JPA 3.4.x(Spring Boot Starterに含まれる)
Hibernate 6.6.x(Spring Data JPAに含まれる)
データベース PostgreSQL 16.x または H2 Database(開発用)
ビルドツール Maven または Gradle

事前に以下の準備を完了してください。

  • JDK 17以上のインストール
  • Spring Boot + Spring Data JPAプロジェクトの基本構成
  • spring-boot-starter-data-jpa依存関係の追加
  • トランザクション管理の基礎知識(@Transactional完全ガイドを参照)

なぜロック戦略が必要なのか

まず、ロック戦略が必要となるシナリオを具体的に見ていきましょう。以下は、在庫管理システムにおける典型的な同時更新問題です。

sequenceDiagram
    participant User1 as ユーザーA
    participant User2 as ユーザーB
    participant DB as データベース
    
    Note over DB: 在庫数: 10個
    
    User1->>DB: 在庫を読み取り(10個)
    User2->>DB: 在庫を読み取り(10個)
    
    Note over User1: 2個注文 → 残り8個と計算
    Note over User2: 3個注文 → 残り7個と計算
    
    User1->>DB: 在庫を8個に更新
    User2->>DB: 在庫を7個に更新
    
    Note over DB: 在庫数: 7個<br/>(本来は5個であるべき)

この「ロストアップデート」問題では、ユーザーAの更新がユーザーBの更新によって上書きされ、在庫が正しく管理されません。本来、両者の注文後は10 - 2 - 3 = 5個になるべきところ、7個として記録されてしまいます。

ロック戦略は、このような問題を防ぐために2つのアプローチを提供します。

戦略 概要 適用シーン
楽観的ロック 更新時に競合を検出し、競合があれば例外を発生 競合が少ない環境、読み取りが多いアプリケーション
悲観的ロック 読み取り時点でレコードをロックし、他のトランザクションをブロック 競合が頻繁に発生する環境、データの即座の整合性が必須

楽観的ロック(Optimistic Locking)の仕組み

楽観的ロックは「競合はめったに起きない」という前提に立ち、データの読み取り時にはロックを取得せず、更新時にのみ競合を検出する方式です。JPAでは@Versionアノテーションを使用して実装します。

@Versionアノテーションの基本

@Versionアノテーションをエンティティのフィールドに付与すると、Hibernateが自動的にバージョン管理を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import jakarta.persistence.Table;

@Entity
@Table(name = "products")
public class Product {

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

    private String name;

    private Integer stock;

    private Integer price;

    @Version
    private Long version;

    // デフォルトコンストラクタ(JPA必須)
    protected Product() {
    }

    public Product(String name, Integer stock, Integer price) {
        this.name = name;
        this.stock = stock;
        this.price = price;
    }

    // Getter/Setter
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public Long getVersion() {
        return version;
    }
}

@Versionで使用可能な型

@Versionアノテーションは以下の型をサポートしています。

説明 推奨度
int / Integer 整数型(32ビット)
long / Long 整数型(64ビット) 高(オーバーフロー対策)
short / Short 整数型(16ビット)
Timestamp タイムスタンプ型 低(精度問題のリスク)
Instant Java 8日時API

実務ではLong型の使用を推奨します。Timestamp型は同一ミリ秒内に複数の更新が発生した場合に競合を検出できないリスクがあります。

楽観的ロックの動作原理

@Versionを使用した場合、HibernateはUPDATE文のWHERE句にバージョン条件を自動的に追加します。

1
2
3
4
-- Hibernateが生成するSQL
UPDATE products 
SET name = ?, stock = ?, price = ?, version = ? 
WHERE id = ? AND version = ?

更新対象のレコードが見つからない(=WHERE句のバージョン条件に一致しない)場合、HibernateはOptimisticLockExceptionをスローします。

sequenceDiagram
    participant User1 as ユーザーA
    participant User2 as ユーザーB
    participant DB as データベース
    
    Note over DB: id=1, stock=10, version=0
    
    User1->>DB: SELECT(version=0を取得)
    User2->>DB: SELECT(version=0を取得)
    
    Note over User1: stock=8に変更
    Note over User2: stock=7に変更
    
    User1->>DB: UPDATE SET version=1<br/>WHERE version=0
    Note over DB: 更新成功<br/>version=1に変更
    
    User2->>DB: UPDATE SET version=1<br/>WHERE version=0
    Note over DB: WHERE条件不一致<br/>更新対象0件
    DB-->>User2: OptimisticLockException

サービス層での楽観的ロックの活用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    public Product decreaseStock(Long productId, int quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        if (product.getStock() < quantity) {
            throw new InsufficientStockException(productId, product.getStock(), quantity);
        }

        product.setStock(product.getStock() - quantity);
        
        // トランザクションコミット時にバージョンチェックが行われる
        return productRepository.save(product);
    }
}

OptimisticLockExceptionのハンドリング

楽観的ロックで競合が発生した場合、適切なエラーハンドリングが必要です。Spring Data JPAでは、ObjectOptimisticLockingFailureException(Springのラッパー例外)またはOptimisticLockException(JPAの例外)がスローされます。

グローバル例外ハンドラーでの処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(OptimisticLockingFailureException.class)
    public ProblemDetail handleOptimisticLockingFailure(
            OptimisticLockingFailureException ex) {
        
        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.CONFLICT,
            "他のユーザーによってデータが更新されました。最新のデータを取得して再度お試しください。"
        );
        problemDetail.setTitle("Conflict");
        problemDetail.setProperty("errorCode", "OPTIMISTIC_LOCK_FAILURE");
        
        return problemDetail;
    }
}

期待されるレスポンス:

1
2
3
4
5
6
7
{
  "type": "about:blank",
  "title": "Conflict",
  "status": 409,
  "detail": "他のユーザーによってデータが更新されました。最新のデータを取得して再度お試しください。",
  "errorCode": "OPTIMISTIC_LOCK_FAILURE"
}

リトライ機構の実装

競合が発生した場合に自動的にリトライする機構を実装することで、ユーザー体験を向上させることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductServiceWithRetry {

    private final ProductRepository productRepository;

    public ProductServiceWithRetry(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Retryable(
        retryFor = OptimisticLockingFailureException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 100, multiplier = 2)
    )
    @Transactional
    public Product decreaseStockWithRetry(Long productId, int quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        if (product.getStock() < quantity) {
            throw new InsufficientStockException(productId, product.getStock(), quantity);
        }

        product.setStock(product.getStock() - quantity);
        return productRepository.save(product);
    }
}

Spring Retryを使用するには、以下の依存関係を追加してください。

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

また、メインクラスに@EnableRetryを追加する必要があります。

1
2
3
4
5
6
7
@SpringBootApplication
@EnableRetry
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

悲観的ロック(Pessimistic Locking)の仕組み

悲観的ロックは「競合は頻繁に起きる」という前提に立ち、データの読み取り時点でレコードをロックする方式です。他のトランザクションは、ロックが解放されるまで待機またはエラーとなります。

LockModeTypeの種類

Spring Data JPAでは、@LockアノテーションとLockModeType列挙型を使用して悲観的ロックを指定します。

LockModeType 説明 生成されるSQL(PostgreSQL)
PESSIMISTIC_READ 共有ロック。読み取りは許可、書き込みはブロック SELECT ... FOR SHARE
PESSIMISTIC_WRITE 排他ロック。読み取り・書き込み両方をブロック SELECT ... FOR UPDATE
PESSIMISTIC_FORCE_INCREMENT 排他ロック + バージョンインクリメント SELECT ... FOR UPDATE + version更新

リポジトリでの@Lockアノテーションの使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // メソッド名クエリに悲観的ロックを適用
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<Product> findWithLockById(Long id);

    // JPQLクエリに悲観的ロックを適用
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdForUpdate(@Param("id") Long id);

    // 共有ロック(読み取り用)
    @Lock(LockModeType.PESSIMISTIC_READ)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdForShare(@Param("id") Long id);
}

サービス層での悲観的ロックの活用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductServiceWithPessimisticLock {

    private final ProductRepository productRepository;

    public ProductServiceWithPessimisticLock(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    public Product decreaseStockWithLock(Long productId, int quantity) {
        // 悲観的ロックを取得(他のトランザクションはここでブロック)
        Product product = productRepository.findWithLockById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        if (product.getStock() < quantity) {
            throw new InsufficientStockException(productId, product.getStock(), quantity);
        }

        product.setStock(product.getStock() - quantity);
        
        return productRepository.save(product);
        // トランザクション終了時にロック解放
    }
}

EntityManagerを使った悲観的ロック

より細かい制御が必要な場合は、EntityManagerを直接使用することもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;

@Service
public class ProductServiceWithEntityManager {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public Product decreaseStockWithTimeout(Long productId, int quantity) {
        // ロック取得タイムアウトを設定(ミリ秒)
        Map<String, Object> properties = Map.of(
            "jakarta.persistence.lock.timeout", 5000
        );

        Product product = entityManager.find(
            Product.class, 
            productId, 
            LockModeType.PESSIMISTIC_WRITE,
            properties
        );

        if (product == null) {
            throw new ProductNotFoundException(productId);
        }

        if (product.getStock() < quantity) {
            throw new InsufficientStockException(productId, product.getStock(), quantity);
        }

        product.setStock(product.getStock() - quantity);
        
        return product;
    }

    @Transactional
    public void updateWithExplicitLock(Long productId, String newName) {
        Product product = entityManager.find(Product.class, productId);
        
        if (product == null) {
            throw new ProductNotFoundException(productId);
        }
        
        // 後からロックを取得
        entityManager.lock(product, LockModeType.PESSIMISTIC_WRITE);
        
        product.setName(newName);
    }
}

デッドロック対策とパフォーマンスへの影響

悲観的ロックを使用する際は、デッドロックのリスクとパフォーマンスへの影響を十分に考慮する必要があります。

デッドロックの発生パターン

sequenceDiagram
    participant TxA as トランザクションA
    participant TxB as トランザクションB
    participant P1 as Product 1
    participant P2 as Product 2
    
    TxA->>P1: ロック取得
    TxB->>P2: ロック取得
    TxA->>P2: ロック取得を試行(待機)
    TxB->>P1: ロック取得を試行(待機)
    
    Note over TxA,TxB: デッドロック発生

デッドロック対策

  1. ロック取得順序の統一

複数のリソースをロックする場合は、常に同じ順序でロックを取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional
public void transferStock(Long sourceId, Long targetId, int quantity) {
    // IDの小さい順にロックを取得することでデッドロックを防止
    Long firstId = Math.min(sourceId, targetId);
    Long secondId = Math.max(sourceId, targetId);
    
    Product first = productRepository.findWithLockById(firstId)
        .orElseThrow(() -> new ProductNotFoundException(firstId));
    Product second = productRepository.findWithLockById(secondId)
        .orElseThrow(() -> new ProductNotFoundException(secondId));
    
    Product source = sourceId.equals(firstId) ? first : second;
    Product target = sourceId.equals(firstId) ? second : first;
    
    if (source.getStock() < quantity) {
        throw new InsufficientStockException(sourceId, source.getStock(), quantity);
    }
    
    source.setStock(source.getStock() - quantity);
    target.setStock(target.getStock() + quantity);
}
  1. ロックタイムアウトの設定

ロック取得に制限時間を設けることで、デッドロック状態からの回復を可能にします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Transactional
public Product findWithTimeout(Long productId) {
    try {
        Map<String, Object> properties = Map.of(
            "jakarta.persistence.lock.timeout", 3000  // 3秒
        );
        
        return entityManager.find(
            Product.class, 
            productId, 
            LockModeType.PESSIMISTIC_WRITE,
            properties
        );
    } catch (LockTimeoutException e) {
        throw new ResourceBusyException(
            "リソースが他の処理で使用中です。しばらく待ってから再度お試しください。"
        );
    }
}
  1. NOWAIT オプションの使用

待機せずに即座にエラーを返す場合は、データベース固有の機能を活用します。

1
2
3
@Query(value = "SELECT * FROM products WHERE id = :id FOR UPDATE NOWAIT", 
       nativeQuery = true)
Optional<Product> findByIdForUpdateNoWait(@Param("id") Long id);

パフォーマンスへの影響

観点 楽観的ロック 悲観的ロック
読み取り性能 高(ロック不要) 低(ロック取得のオーバーヘッド)
書き込み競合時 例外発生後リトライが必要 待機後に処理継続
スケーラビリティ 高(同時実行可能) 低(直列化される)
デッドロックリスク なし あり
コネクション保持時間 短い 長い(ロック保持中)

ロック戦略の選定基準

適切なロック戦略を選択するためのフローチャートを示します。

flowchart TD
    A[同時更新の対策が必要] --> B{競合頻度は?}
    B -->|低い| C{リトライ可能?}
    B -->|高い| D{即座の整合性が必須?}
    
    C -->|Yes| E[楽観的ロック + リトライ]
    C -->|No| F[楽観的ロック + エラー通知]
    
    D -->|Yes| G[悲観的ロック]
    D -->|No| H{長時間トランザクション?}
    
    H -->|Yes| I[楽観的ロック推奨]
    H -->|No| G
    
    E --> J[(@Version)]
    F --> J
    G --> K[(@Lock + PESSIMISTIC_WRITE)]
    I --> J
    
    style E fill:#90EE90
    style F fill:#90EE90
    style G fill:#FFB6C1
    style I fill:#90EE90
~~~

### 楽観的ロックが適している場合

- 読み取りが多く、更新が少ないアプリケーション
- 競合が発生しても業務フローで再試行が可能
- Webアプリケーションのフォーム送信
- 複数ユーザーが同じドキュメントを編集するシナリオ

### 悲観的ロックが適している場合

- 金融取引など、データの即座の整合性が必須
- 在庫管理で「売り越し」を絶対に防ぎたい場合
- バッチ処理での大量データ更新
- 短いトランザクションで処理が完了する場合

### 実践例:ECサイトの注文処理

~~~java
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderService(OrderRepository orderRepository, 
                       ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    /**
     * 通常の注文処理(楽観的ロック)
     * - 競合発生時はユーザーに再試行を促す
     */
    @Transactional
    public Order createOrder(OrderRequest request) {
        Product product = productRepository.findById(request.getProductId())
            .orElseThrow(() -> new ProductNotFoundException(request.getProductId()));

        if (product.getStock() < request.getQuantity()) {
            throw new InsufficientStockException(
                request.getProductId(), 
                product.getStock(), 
                request.getQuantity()
            );
        }

        product.setStock(product.getStock() - request.getQuantity());
        productRepository.save(product);

        Order order = new Order(request.getCustomerId(), product, request.getQuantity());
        return orderRepository.save(order);
    }

    /**
     * タイムセール注文処理(悲観的ロック)
     * - 在庫が少なく競合が頻発する状況
     * - 「売り越し」を絶対に防ぎたい
     */
    @Transactional
    public Order createTimeSaleOrder(OrderRequest request) {
        Product product = productRepository.findWithLockById(request.getProductId())
            .orElseThrow(() -> new ProductNotFoundException(request.getProductId()));

        if (product.getStock() < request.getQuantity()) {
            throw new InsufficientStockException(
                request.getProductId(), 
                product.getStock(), 
                request.getQuantity()
            );
        }

        product.setStock(product.getStock() - request.getQuantity());

        Order order = new Order(request.getCustomerId(), product, request.getQuantity());
        return orderRepository.save(order);
    }
}
~~~

## よくある誤解とアンチパターン

### 誤解1:@Versionを手動で更新してはいけない

`@Version`フィールドの値はHibernateが自動的に管理します。手動で設定すると意図しない動作を引き起こす可能性があります。

~~~java
// アンチパターン:バージョンを手動で設定
product.setVersion(product.getVersion() + 1); // 絶対にやってはいけない

// 正しいアプローチ:Hibernateに任せる
product.setStock(newStock);
productRepository.save(product); // バージョンは自動でインクリメント
~~~

### 誤解2:悲観的ロックは常に安全

悲観的ロックでもトランザクション境界外でのデータ変更は防げません。

~~~java
// アンチパターン:ロックの外で処理
@Transactional
public Product getLockedProduct(Long id) {
    return productRepository.findWithLockById(id)
        .orElseThrow(() -> new ProductNotFoundException(id));
}

// 別メソッドで呼び出し(トランザクション外=ロック解放済み)
public void updateProduct(Long id, String newName) {
    Product product = getLockedProduct(id); // ロックは既に解放
    product.setName(newName);
    productRepository.save(product); // 競合の可能性あり
}
~~~

### 誤解3:楽観的ロックと悲観的ロックは排他的

両方を組み合わせることも可能です。`PESSIMISTIC_FORCE_INCREMENT`は、悲観的ロックを取得しつつバージョンもインクリメントします。

~~~java
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithForceIncrement(@Param("id") Long id);
~~~

### 誤解4:長いトランザクションでの悲観的ロック

悲観的ロックを長時間保持すると、他のトランザクションが待機し続け、システム全体のスループットが低下します。

~~~java
// アンチパターン:外部API呼び出しを含むトランザクション
@Transactional
public void processWithExternalCall(Long productId) {
    Product product = productRepository.findWithLockById(productId)
        .orElseThrow(() -> new ProductNotFoundException(productId));
    
    // 外部API呼び出し(数秒かかる可能性)
    ExternalResponse response = externalApi.call(); // ロック保持中
    
    product.setPrice(response.getNewPrice());
}

// 正しいアプローチ:外部呼び出しをトランザクション外に
public void processWithExternalCall(Long productId) {
    // 1. 外部API呼び出し(トランザクション外)
    ExternalResponse response = externalApi.call();
    
    // 2. データ更新(短いトランザクション)
    updatePrice(productId, response.getNewPrice());
}

@Transactional
public void updatePrice(Long productId, Integer newPrice) {
    Product product = productRepository.findWithLockById(productId)
        .orElseThrow(() -> new ProductNotFoundException(productId));
    product.setPrice(newPrice);
}
~~~

### 誤解5:バージョンフィールドのNULL

`@Version`フィールドが`null`の場合、エンティティは新規として扱われ、競合検出が機能しません。

~~~java
// アンチパターン:プリミティブ型でNULL不可にしているつもりでも...
@Version
private long version; // 初期値0、問題なし

// 注意:ラッパー型の場合
@Version
private Long version; // NULLの可能性あり

// Hibernate 6.x以降ではNULLは新規エンティティとして扱われる
~~~

## まとめと実践Tips

Spring JPAのロック戦略について、楽観的ロックと悲観的ロックの実装方法を解説しました。

### 実装のポイント

1. **楽観的ロック**
   - `@Version`アノテーションをエンティティに追加するだけで有効化
   - `Long`型の使用を推奨
   - `OptimisticLockingFailureException`を適切にハンドリング
   - リトライ機構の実装を検討

2. **悲観的ロック**
   - `@Lock`アノテーションと`LockModeType`でロック種別を指定
   - `PESSIMISTIC_WRITE`が最も一般的
   - ロックタイムアウトの設定を忘れずに
   - ロック取得順序を統一してデッドロックを防止

3. **選定基準**
   - 競合頻度が低い → 楽観的ロック
   - 競合頻度が高い、または即座の整合性が必須 → 悲観的ロック
   - 長時間トランザクション → 楽観的ロック推奨

### チェックリスト

| 項目 | 確認内容 |
|------|----------|
| バージョンフィールド | `@Version`を適切な型で定義しているか |
| 例外ハンドリング | `OptimisticLockingFailureException`を捕捉しているか |
| ロックタイムアウト | 悲観的ロック使用時にタイムアウトを設定しているか |
| デッドロック対策 | 複数リソースのロック順序が統一されているか |
| トランザクション境界 | ロックがトランザクション内で完結しているか |
| パフォーマンステスト | 負荷テストで競合発生時の動作を確認したか |

## 参考リンク

- [Spring Data JPA Reference - Locking](https://docs.spring.io/spring-data/jpa/reference/jpa/locking.html)
- [Hibernate ORM User Guide - Locking](https://docs.hibernate.org/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#locking)
- [Jakarta Persistence 3.2 Specification](https://jakarta.ee/specifications/persistence/3.2/)
- [Spring Retry Reference](https://docs.spring.io/spring-retry/reference/)
- [PostgreSQL LOCK Documentation](https://www.postgresql.org/docs/current/explicit-locking.html)