

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

# 使用 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)，以自動清除過期的鎖定項目。