DynamoDB 잠금 클라이언트를 사용한 분산 잠금
기존 잠금-획득-릴리스 의미 체계가 필요한 애플리케이션의 경우 DynamoDB 잠금 클라이언트는 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(Time to Live)을 활성화하여 만료된 잠금 항목을 자동으로 정리하는 것이 좋습니다.