

# DynamoDB で同時更新を処理するためのベストプラクティス
<a name="BestPractices_ImplementingVersionControl"></a>

分散システムでは、複数のプロセスまたはユーザーが同時に同じデータを変更しようとすることがあります。同時実行制御がない場合、これらの同時書き込みにより、更新の損失、データの不整合、競合状態が発生する可能性があります。DynamoDB には、同時アクセスの管理とデータ整合性の維持に役立つメカニズムがいくつか用意されています。

**注記**  
`UpdateItem` などの個々の書き込みオペレーションはアトミックであり、同時実行性に関係なく、常に項目の最新バージョンに対して実行されます。アプリケーションが項目を読み取り、読み取った値に基づいて書き戻す必要がある場合 (読み取り/変更/書き込みサイクル)、読み取りと書き込みの間に別のプロセスが項目を変更する可能性があるため、ロック戦略が必要になります。

同時更新を処理するには、主に 2 つの戦略があります。
+ **楽観的ロック** — 競合がまれであると想定します。同時アクセスを許可し、条件付き書き込みを使用して書き込み時に競合を検出します。競合が検出されると、書き込みは失敗し、アプリケーションは再試行できます。
+ **悲観的ロック** — 競合が発生する可能性が高いと想定します。リソースを変更する前に排他的アクセスを取得することで、同時アクセスを防止します。他のプロセスは、ロックが解除されるまで待つ必要があります。

次の表は、DynamoDB で使用できるアプローチをまとめたものです。


| アプローチ | メカニズム | 次の用途に適しています | 
| --- | --- | --- | 
| オプティミスティックロック | バージョン属性 \$1 条件付き書き込み | 競合が少なく、再試行コストが低い | 
| 悲観的ロック (トランザクション) | TransactWriteItems | 複数項目のアトミック性、中程度の競合 | 
| 悲観的ロック (ロッククライアント) | リースとハートビートを備えた専用ロックテーブル | 長時間実行されるワークフロー、分散調整 | 

# バージョン番号を使用した楽観的ロック
<a name="BestPractices_OptimisticLocking"></a>

楽観的ロックは、競合を回避するのではなく、書き込み時に競合を検出する戦略です。各項目には、更新ごとに増分するバージョン属性が含まれています。項目を更新するときは、バージョン番号がアプリケーションが最後に読み取った値と一致するかどうかをチェックする[条件式](Expressions.ConditionExpressions.md)を含めます。その間に別のプロセスが項目を変更した場合、条件は失敗し、DynamoDB は `ConditionalCheckFailedException` を返します。

## どのような場合に楽観的ロックを使用するか
<a name="BestPractices_OptimisticLocking_WhenToUse"></a>

楽観的ロックは、次の場合に適しています。
+ 複数のユーザーまたはプロセスが同じ項目を更新する可能性がありますが、競合はまれです。
+ 失敗した書き込みを再試行することは、アプリケーションにとって安価です。
+ 分散ロックの管理に伴うオーバーヘッドや複雑さを回避したい。

一般的な例としては、e コマース在庫の更新、共同編集プラットフォーム、金融取引レコードなどがあります。

## トレードオフ
<a name="BestPractices_OptimisticLocking_Tradeoffs"></a>

**競合率が高い場合の再試行のオーバーヘッド**  
高同時実行性環境では、競合の可能性が高くなり、再試行や書き込みコストの増大につながる可能性があります。

**実装の複雑さ**  
項目にバージョン管理を追加し、条件付きチェックを処理すると、アプリケーションロジックが複雑になります。AWS SDK for Java v2 拡張クライアントは、自動的にバージョン番号を管理する [https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE) 注釈を通じて組み込みサポートを提供します。

## パターンの設計
<a name="BestPractices_OptimisticLocking_PatternDesign"></a>

各項目にバージョン属性を含めます。シンプルなスキーマ設計は次のとおりです。
+ パーティションキー – 各項目の一意の識別子 (`ItemId` など)。
+ 属性:
  + `ItemId` – ジョブの一意の識別子
  + `Version` – 項目のバージョン番号を表す整数
  + `QuantityLeft` – 項目の残りのインベントリ

項目が最初に作成されると、`Version` 属性は 1 に設定されます。更新される都度、バージョン番号は 1 ずつ増分します。


| ItemID (パーティションキー) | バージョン | QuantityLeft | 
| --- | --- | --- | 
| Bananas | 1 | 10 | 
| Apples | 1 | 5 | 
| Oranges | 1 | 7 | 

## 実装
<a name="BestPractices_OptimisticLocking_Implementation"></a>

楽観的ロックを実装するには、次の手順に従います。

1. 項目の最新バージョンを読み取ります。

   ```
   def get_item(item_id):
       response = table.get_item(Key={'ItemID': item_id})
       return response['Item']
   
   item = get_item('Bananas')
   current_version = item['Version']
   ```

1. バージョン番号をチェックする条件式を使用して項目を更新します。

   ```
   def update_item(item_id, qty_bought, current_version):
       try:
           response = table.update_item(
               Key={'ItemID': item_id},
               UpdateExpression="SET QuantityLeft = QuantityLeft - :qty, Version = :new_v",
               ConditionExpression="Version = :expected_v",
               ExpressionAttributeValues={
                   ':qty': qty_bought,
                   ':new_v': current_version + 1,
                   ':expected_v': current_version
               },
               ReturnValues="UPDATED_NEW"
           )
           return response
       except ClientError as e:
           if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
               print("Version conflict: another process updated this item.")
           raise
   ```

1. 新しい読み取りを再試行して競合を処理します。

   各再試行には追加の読み取りが必要なため、再試行の合計数を制限します。

   ```
   def update_with_retry(item_id, qty_bought, max_retries=3):
       for attempt in range(max_retries):
           item = get_item(item_id)
           try:
               return update_item(item_id, qty_bought, item['Version'])
           except ClientError as e:
               if e.response['Error']['Code'] != 'ConditionalCheckFailedException':
                   raise
               print(f"Retry {attempt + 1}/{max_retries}")
       raise Exception("Update failed after maximum retries.")
   ```

Java アプリケーションの場合、AWS SDK for Java v2 拡張クライアントは、[https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE) 注釈を通じて組み込みの楽観的ロックサポートを提供し、自動的にバージョン番号を管理します。

条件式の詳細については、「[DynamoDB 条件式 CLI の例](Expressions.ConditionExpressions.md)」を参照してください。

# DynamoDB トランザクションによる悲観的ロック
<a name="BestPractices_PessimisticLocking"></a>

DynamoDB [トランザクション](transactions.md)は、グループ化されたオペレーションにオールオアナッシングアプローチを提供します。`TransactWriteItems` を使用すると、DynamoDB はトランザクション内のすべての項目をモニタリングします。トランザクション中に別のオペレーションによって項目が変更された場合、トランザクション全体がキャンセルされ、DynamoDB は `TransactionCanceledException` を返します。この動作は、競合する同時変更が事後に検出されるのではなく防止されるため、悲観的な同時実行制御の一形式を提供します。

## どのような場合にロックにトランザクションを使用するか
<a name="BestPractices_PessimisticLocking_WhenToUse"></a>

トランザクションは、次の場合に適しています。
+ 同じテーブル内またはテーブル間で、複数の項目をアトミックに更新する必要があります。
+ ビジネスロジックでは、オールオアナッシングセマンティクス (すべての変更が成功するか、まったく適用されないかのいずれか) が必要です。

一般的な例としては、アカウント間での資金の送金、在庫テーブルと注文テーブルの両方を更新する注文の配置、ゲーム内のプレイヤー間でのアイテムの交換などがあります。

## トレードオフ
<a name="BestPractices_PessimisticLocking_Tradeoffs"></a>

**書き込みコストの増加**  
最大 1 KB の項目の場合、トランザクションは項目ごとに 2 WCU (1 つは準備用、もう 1 つはコミット用) を消費しますが、標準書き込みの場合は 1 WCU です。

**項目の制限**  
1 つのトランザクションには、1 つ以上のテーブルで最大 100 個のアクションを含めることができます。

**競合の機密性**  
トランザクション内の項目が別のオペレーションによって変更されると、トランザクション全体が失敗します。競合率が高いシナリオでは、頻繁にキャンセルが発生する可能性があります。

## 実装
<a name="BestPractices_PessimisticLocking_Implementation"></a>

次の例では、`TransactWriteItems` を使用して 2 つの項目間で在庫をアトミックに転送します。トランザクション中に別のプロセスがいずれかの項目を変更すると、オペレーション全体がロールバックされます。

```
import boto3

client = boto3.client('dynamodb')

def transfer_inventory(source_id, target_id, quantity):
    try:
        client.transact_write_items(
            TransactItems=[
                {
                    'Update': {
                        'TableName': 'Inventory',
                        'Key': {'ItemID': {'S': source_id}},
                        'UpdateExpression': 'SET QuantityLeft = QuantityLeft - :qty',
                        'ConditionExpression': 'QuantityLeft >= :qty',
                        'ExpressionAttributeValues': {
                            ':qty': {'N': str(quantity)}
                        }
                    }
                },
                {
                    'Update': {
                        'TableName': 'Inventory',
                        'Key': {'ItemID': {'S': target_id}},
                        'UpdateExpression': 'SET QuantityLeft = QuantityLeft + :qty',
                        'ExpressionAttributeValues': {
                            ':qty': {'N': str(quantity)}
                        }
                    }
                }
            ]
        )
        return True
    except client.exceptions.TransactionCanceledException as e:
        print(f"Transaction canceled: {e}")
        return False
```

この例では、条件式は十分な在庫が存在することを確認しますが、バージョン属性は必要ありません。DynamoDB は、準備フェーズとコミットフェーズの間に別のオペレーションによってトランザクション内の項目が変更された場合、トランザクションを自動的にキャンセルします。これにより、悲観的な同時実行制御が実現します。競合する同時変更はトランザクション自体によって防止されます。

**注記**  
バージョンチェックを追加の条件式として追加することで、トランザクションと楽観的ロックを組み合わせることができます。これにより、追加の保護レイヤーが提供されますが、トランザクションが競合を検出するためには必須ではありません。

詳細については、「[DynamoDB トランザクションで複雑なワークフローを管理する](transactions.md)」を参照してください。

# DynamoDB ロッククライアントによる分散ロック
<a name="BestPractices_DistributedLocking"></a>

従来のロック/取得/解除セマンティクスを必要とするアプリケーションの場合、DynamoDB ロッククライアントは、DynamoDB テーブルをロックストアとして使用して分散ロックを実装するオープンソースライブラリです。このアプローチは、複数のアプリケーションインスタンス間で外部リソース (S3 オブジェクトや共有設定など) へのアクセスを調整する必要がある場合に便利です。

ロッククライアントはオープンソースの [Java ライブラリ](https://github.com/awslabs/amazon-dynamodb-lock-client)として使用できます。

## 仕組み
<a name="BestPractices_DistributedLocking_HowItWorks"></a>

ロッククライアントは、専用の DynamoDB テーブルを使用してロックを追跡します。各ロックは、次の主要属性を持つ項目として表されます。
+ ロックされているリソースを識別するパーティションキー。
+ ロックが有効である期間を指定するリース期間。ロックホルダーがクラッシュするか、応答しなくなった場合、ロックはリース期間後に自動的に期限切れになります。
+ ロックホルダーがリースを延長するために定期的に送信するハートビート。これにより、ホルダーがまだアクティブに処理している間にロックの有効期限が切れるのを防ぎます。

ロッククライアントは条件付き書き込みを使用して、一度に 1 つのプロセスのみがロックを取得できるようにします。ロックがすでに保持されている場合、呼び出し元は待機して再試行するか、すぐに失敗するかを選択できます。

## どのような場合にロッククライアントを使用するか
<a name="BestPractices_DistributedLocking_WhenToUse"></a>

ロッククライアントは、次の場合に適しています。
+ 複数のアプリケーションインスタンスまたはマイクロサービス間で共有リソースへのアクセスを調整する必要があります。
+ クリティカルセクションは長時間 (数秒から数分) 実行されるため、競合時にオペレーション全体を再試行するとコストがかかります。
+ プロセスの失敗を適切に処理するには、自動ロックの有効期限が必要です。

一般的な例としては、分散ワークフローのオーケストレーション、複数のインスタンス間での cron ジョブの調整、共有外部リソースへのアクセスの管理などがあります。

## トレードオフ
<a name="BestPractices_DistributedLocking_Tradeoffs"></a>

**追加のインフラストラクチャ**  
ロック管理には専用の DynamoDB テーブルが必要で、ロック操作とハートビート用の追加の読み取りおよび書き込み容量も必要です。

**クロック依存性**  
ロックの有効期限はタイムスタンプに依存します。クライアント間で大幅なクロックスキューが発生すると、特にリース期間が短い場合、予期しない動作が発生する可能性があります。

**デッドロックリスク**  
アプリケーションが複数のリソースのロックを取得する場合、デッドロックを回避するために、一貫した順序でロックを取得する必要があります。リース期間は、応答しないホルダーからロックを自動的に解放することで、セーフティネットを提供します。

## 実装
<a name="BestPractices_DistributedLocking_Implementation"></a>

次の例は、DynamoDB ロッククライアントを使用してロックを取得および解除する方法を示しています。

```
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

final DynamoDbClient dynamoDB = DynamoDbClient.builder()
    .region(Region.US_WEST_2)
    .build();

final AmazonDynamoDBLockClient lockClient = new AmazonDynamoDBLockClient(
    AmazonDynamoDBLockClientOptions.builder(dynamoDB, "Locks")
        .withTimeUnit(TimeUnit.SECONDS)
        .withLeaseDuration(10L)
        .withHeartbeatPeriod(3L)
        .withCreateHeartbeatBackgroundThread(true)
        .build());

// Try to acquire a lock on a resource
final Optional<LockItem> lock =
    lockClient.tryAcquireLock(AcquireLockOptions.builder("my-shared-resource").build());

if (lock.isPresent()) {
    try {
        // Perform operations that require exclusive access
        processSharedResource();
    } finally {
        // Always release the lock when done
        lockClient.releaseLock(lock.get());
    }
} else {
    System.out.println("Failed to acquire lock.");
}

lockClient.close();
```

**重要**  
処理ロジックが例外をスローした場合でもロックが確実に解除されるように、常に `finally` ブロックでロックを解除します。解除されていないロックは、リースの有効期限が切れるまで他のプロセスをブロックします。

条件付き書き込みを直接使用して、ロッククライアントライブラリを使用せずシンプルなロックメカニズムを実装することもできます。次の例では、条件式を指定した `UpdateItem` を使用してロックを取得し、`DeleteItem` を使用してロックを解除します。

```
from datetime import datetime, timedelta
from boto3.dynamodb.conditions import Attr

def acquire_lock(table, resource_name, owner_id, ttl_seconds):
    """Attempt to acquire a lock. Returns True if successful."""
    expiry = (datetime.now() + timedelta(seconds=ttl_seconds)).isoformat()
    now = datetime.now().isoformat()
    try:
        table.update_item(
            Key={'LockID': resource_name},
            UpdateExpression='SET #owner = :owner, #expiry = :expiry',
            ConditionExpression=Attr('LockID').not_exists() | Attr('ExpiresAt').lt(now),
            ExpressionAttributeNames={'#owner': 'OwnerID', '#expiry': 'ExpiresAt'},
            ExpressionAttributeValues={':owner': owner_id, ':expiry': expiry}
        )
        return True
    except table.meta.client.exceptions.ConditionalCheckFailedException:
        return False

def release_lock(table, resource_name, owner_id):
    """Release a lock. Only succeeds if the caller is the lock owner."""
    try:
        table.delete_item(
            Key={'LockID': resource_name},
            ConditionExpression=Attr('OwnerID').eq(owner_id)
        )
        return True
    except table.meta.client.exceptions.ConditionalCheckFailedException:
        return False
```

このアプローチでは、条件式を使用して、ロックが存在しないか期限切れの場合にのみロックを取得でき、ロックを取得したプロセスによってのみロックが解除されることを保証します。期限切れのロック項目を自動的にクリーンアップするには、ロックテーブルで[有効期限 (TTL)](TTL.md) を有効にすることを検討してください。

## 同時実行制御戦略の選択
<a name="BestPractices_ChoosingLockingStrategy"></a>

ワークロードに適したアプローチを選択するには、次のガイドラインを使用します。

次の場合は、**楽観的ロックを使用します**。  
+ 競合はまれです。
+ 失敗した書き込みを再試行するのは安価です。
+ 一度に 1 つの項目を更新します。

次の場合は、**トランザクションを使用します**。  
+ 複数の項目をアトミックに更新する必要があります。
+ 項目またはテーブル全体でオールオアナッシングセマンティクスが必要です。
+ 条件チェックと書き込みを 1 つのオペレーションで組み合わせる必要があります。

次の場合は、**ロッククライアントを使用します**。  
+ 分散プロセス全体で外部リソースへのアクセスを調整する必要があります。
+ 重要なセクションは長時間実行され、競合時の再試行にはコストがかかります。
+ プロセスの失敗を処理するには、自動ロックの有効期限が必要です。

**注記**  
[DynamoDB グローバルテーブル](GlobalTables.md)を使用する場合は、グローバルテーブルが同時更新に「最終書き込み者優先」調整戦略を使用することに注意してください。バージョン番号を使用した楽観的ロックは、あるリージョンでの書き込みがバージョンチェックなしで別のリージョンでの同時書き込みを上書きする可能性があるため、リージョン間では期待どおりに機能しません。グローバルテーブルを使用する場合は、アプリケーションレベルで競合を処理するようにアプリケーションを設計します。