使用 DynamoDB 鎖定用戶端進行分散式鎖定 - Amazon DynamoDB

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

使用 DynamoDB 鎖定用戶端進行分散式鎖定

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

鎖定用戶端可作為開放原始碼 Java 程式庫使用。

運作方式

鎖定用戶端使用專用的 DynamoDB 資料表來追蹤鎖定。每個鎖定會以具有下列金鑰屬性的項目表示:

  • 識別要鎖定之資源的分割區索引鍵。

  • 指定鎖定有效時間的租用持續時間。如果鎖定持有者當機或變得沒有回應,鎖定會在租用期間之後自動過期。

  • 鎖定持有者定期傳送的活動訊號,以延長租用。這可防止鎖定在持有人仍在主動處理時過期。

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

何時使用鎖定用戶端

鎖定用戶端非常適合下列情況:

  • 您需要協調跨多個應用程式執行個體或微服務對共用資源的存取。

  • 關鍵區段長時間執行 (秒到分鐘),在衝突時重試整個操作會很昂貴。

  • 您需要自動鎖定過期,才能正常處理程序失敗。

常見範例包括協調分散式工作流程、跨多個執行個體協調 Cron 任務,以及管理對共用外部資源的存取。

取捨

其他基礎設施

需要專用的 DynamoDB 資料表來進行鎖定管理,並針對鎖定操作和活動訊號提供額外的讀取和寫入容量。

時鐘相依性

鎖定到期取決於時間戳記。用戶端之間的重大時鐘扭曲可能會導致意外行為,尤其是在短期租用期間。

死鎖風險

如果您的應用程式在多個資源上取得鎖定,您必須以一致順序取得它們,以避免死結。租用期間會自動從沒有回應的持有人釋放鎖定,以提供安全網路。

實作

下列範例示範如何使用 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),以自動清除過期的鎖定項目。