

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 在 DynamoDB 中處理並行更新的最佳實務
<a name="BestPractices_ImplementingVersionControl"></a>

在分散式系統中，多個程序或使用者可能會嘗試同時修改相同的資料。如果沒有並行控制，這些並行寫入可能會導致遺失更新、資料不一致或競爭條件。DynamoDB 提供多種機制，協助您管理並行存取和維護資料完整性。

**注意**  
等個別寫入操作`UpdateItem`是原子的，無論並行為何，一律在項目的最新版本上運作。當您的應用程式必須讀取項目，然後根據讀取值 (read-modify-write週期） 將項目寫回時，需要鎖定策略，因為另一個程序可能會修改讀取和寫入之間的項目。

處理並行更新有兩種主要策略：
+ **樂觀鎖定** – 假設衝突很少見。它允許並行存取，並使用條件式寫入偵測寫入時的衝突。如果偵測到衝突，寫入會失敗，應用程式可以重試。
+ **冪等鎖定** – 假設可能發生衝突。它可透過在修改資源之前取得資源的專屬存取權來防止並行存取。其他程序必須等到鎖定釋放。

下表摘要說明 DynamoDB 中可用的方法：


| 方法 | Mechanism | 最適合 | 
| --- | --- | --- | 
| 樂觀鎖定 | 版本屬性 \$1 條件式寫入 | 爭用性低、成本低廉的重試 | 
| 預估鎖定 （交易） | TransactWriteItems | 多項目原子性、中度爭用 | 
| 冪等鎖定 （鎖定用戶端） | 具有租用和活動訊號的專用鎖定資料表 | 長時間執行的工作流程、分散式協調 | 

# 含版本編號的樂觀鎖定
<a name="BestPractices_OptimisticLocking"></a>

樂觀鎖定是一種策略，可偵測寫入時的衝突，而不是阻止衝突。每個項目都包含一個版本屬性，每次更新都會遞增。更新項目時，您會包含[條件表達](Expressions.ConditionExpressions.md)式，以檢查版本號碼是否符合應用程式上次讀取的值。如果另一個程序同時修改了項目，則條件會失敗，且 DynamoDB 會傳回 `ConditionalCheckFailedException`。

## 何時使用樂觀鎖定
<a name="BestPractices_OptimisticLocking_WhenToUse"></a>

樂觀鎖定非常適合下列情況：
+ 多個使用者或程序可能會更新相同的項目，但衝突不常發生。
+ 重試失敗的寫入對您的應用程式來說並不昂貴。
+ 您想要避免管理分散式鎖定的額外負荷和複雜性。

常見的範例包括電子商務庫存更新、協作編輯平台和金融交易記錄。

## 取捨
<a name="BestPractices_OptimisticLocking_Tradeoffs"></a>

**在高度爭用中重試額外負荷**  
在高並行環境中，衝突發生的可能性提高，可能造成更多重試與更高的寫入成本。

**實作複雜度**  
將版本控制新增至項目和處理條件式檢查，會增加應用程式邏輯的複雜性。適用於 Java v2 的 AWS SDK 增強型用戶端透過 [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 | 
| --- | --- | --- | 
| 香蕉 | 1 | 10 | 
| Apples | 1 | 5 | 
| 橘色 | 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 應用程式，適用於 Java v2 的 AWS SDK 增強型用戶端透過 [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)提供all-or-nothing方法。當您使用 時`TransactWriteItems`，DynamoDB 會監控交易中的所有項目。如果在交易期間由其他操作修改任何項目，則整個交易都會取消，且 DynamoDB 會傳回 `TransactionCanceledException`。這種行為提供一種可觀的並行控制形式，因為防止衝突並行修改，而不是在事實之後偵測到。

## 何時使用交易進行鎖定
<a name="BestPractices_PessimisticLocking_WhenToUse"></a>

在下列情況下，交易非常適合：
+ 您需要以原子方式更新多個項目，無論是在同一資料表內或跨資料表。
+ 您的商業邏輯需要all-or-nothing語意 - 所有變更都成功套用或不套用。

常見範例包括在帳戶之間轉移資金、下訂單同時更新庫存和訂單資料表，以及在遊戲中的玩家之間交換項目。

## 取捨
<a name="BestPractices_PessimisticLocking_Tradeoffs"></a>

**較高的寫入成本**  
對於高達 1 KB 的項目，相較於標準寫入的 1 WCUs，交易會耗用每個項目 2 個 WCU （一個用於準備，一個用於遞交）。

**項目限制**  
單一交易最多可包含一或多個資料表中的 100 個動作。

**衝突敏感度**  
如果交易中的任何項目被另一個操作修改，則整個交易會失敗。在高內容案例中，這可能會導致頻繁取消。

## 實作
<a name="BestPractices_PessimisticLocking_Implementation"></a>

下列範例使用 在兩個項目之間`TransactWriteItems`以原子方式傳輸庫存。如果另一個程序在交易期間修改任一項目，則會復原整個操作。

```
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>

對於需要傳統lock-acquire-release語意的應用程式，DynamoDB Lock Client 是一個開放原始碼程式庫，使用 DynamoDB 資料表做為鎖定存放區來實作分散式鎖定。當您需要在多個應用程式執行個體之間協調對外部資源 （例如 S3 物件或共用組態） 的存取時，此方法非常有用。

鎖定用戶端可作為開放原始碼 [Java 程式庫](https://github.com/awslabs/amazon-dynamodb-lock-client)使用。

## 運作方式
<a name="BestPractices_DistributedLocking_HowItWorks"></a>

鎖定用戶端使用專用的 DynamoDB 資料表來追蹤鎖定。每個鎖定會以具有下列金鑰屬性的項目表示：
+ 識別要鎖定之資源的分割區索引鍵。
+ 指定鎖定有效時間的租用持續時間。如果鎖定持有者當機或變得沒有回應，鎖定會在租用期間之後自動過期。
+ 鎖定持有者定期傳送的活動訊號，以延長租用。這可防止鎖定在持有人仍在主動處理時過期。

鎖定用戶端使用條件式寫入，以確保一次只有一個程序可以取得鎖定。如果鎖定已保留，發起人可以選擇等待並重試或立即失敗。

## 何時使用鎖定用戶端
<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>

使用下列準則為您的工作負載選擇正確的方法：

在下列情況下**使用樂觀鎖定**：  
+ 衝突不常發生。
+ 重試失敗的寫入並不昂貴。
+ 您一次更新單一項目。

在下列情況下**使用交易**：  
+ 您需要以原子方式更新多個項目。
+ 您需要跨項目all-or-nothing語意。
+ 您需要在單一操作中結合條件檢查與寫入。

**在下列情況下使用鎖定用戶端**：  
+ 您需要協調跨分散式程序對外部資源的存取。
+ 關鍵區段長時間執行，在衝突上重試非常昂貴。
+ 您需要自動鎖定過期，才能處理程序失敗。

**注意**  
如果您使用 [DynamoDB 全域資料表](GlobalTables.md)，請注意全域資料表會使用「最後寫入器獲勝」對帳策略進行並行更新。具有版本號碼的樂觀鎖定在跨區域無法如預期運作，因為一個區域中的寫入可能會在沒有版本檢查的情況下覆寫另一個區域中的並行寫入。設計您的應用程式，以便在使用全域資料表時處理應用程式層級的衝突。