Bloqueio distribuído com o DynamoDB Lock Client - Amazon DynamoDB

Bloqueio distribuído com o DynamoDB Lock Client

Para aplicações que exigem a semântica tradicional de aquisição e liberação de bloqueios, o DynamoDB Lock Client é uma biblioteca de código aberto que implementa o bloqueio distribuído usando uma tabela do DynamoDB como armazenamento de bloqueios. Essa abordagem é útil quando é necessário coordenar o acesso a um recurso externo (como um objeto do S3 ou uma configuração compartilhada) em várias instâncias da aplicação.

O cliente de bloqueio está disponível como uma biblioteca Java de código aberto.

Como funciona

O cliente de bloqueio usa uma tabela dedicada do DynamoDB para rastrear bloqueios. Cada bloqueio é representado como um item com os seguintes atributos de chave:

  • Uma chave de partição que identifica o recurso que está sendo bloqueado.

  • A duração da concessão que especifica por quanto tempo o bloqueio é válido. Se o detentor do bloqueio falhar ou deixar de responder, o bloqueio expirará automaticamente após a duração da concessão.

  • Um heartbeat que o detentor do bloqueio envia periodicamente para estender a concessão. Isso evita que o bloqueio expire enquanto o detentor ainda está processando ativamente.

O cliente de bloqueio usa gravações condicionais para garantir que somente um processo possa adquirir um bloqueio por vez. Se um bloqueio já estiver retido, o chamador poderá optar por esperar e tentar novamente ou antecipar-se à falha imediatamente.

Quando usar o cliente de bloqueio

O cliente de bloqueio é uma boa opção quando:

  • É necessário coordenar o acesso a um recurso compartilhado em várias instâncias de aplicação ou microsserviços.

  • A seção essencial for de longa duração (de segundos a minutos) e tentar novamente toda a operação em caso de conflito for caro.

  • Você precisa da expiração automática de bloqueio para lidar com as falhas do processo normalmente.

Exemplos comuns incluem orquestrar fluxos de trabalho distribuídos, coordenar tarefas cron em várias instâncias e gerenciar o acesso a recursos externos compartilhados.

Desvantagens

Infraestrutura adicional

Requer uma tabela do DynamoDB dedicada para gerenciamento de bloqueios, com capacidade adicional de leitura e gravação para operações de bloqueio e heartbeats.

Dependência de clock

A expiração do bloqueio depende de carimbos de data/hora. Uma distorção de clock significativa entre os clientes pode causar um comportamento inesperado, especialmente para períodos curtos de concessão.

Risco de deadlock

Se a aplicação adquirir bloqueios em vários recursos, será necessário adquiri-los em uma ordem consistente para evitar deadlocks. A duração da concessão oferece uma proteção ao liberar automaticamente os bloqueios dos detentores que não respondem.

Implementação

O exemplo a seguir mostra como usar o DynamoDB Lock Client para adquirir e liberar um bloqueio:

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();
Importante

Sempre libere os bloqueios em um bloco finally para garantir que os bloqueios sejam liberados, mesmo que sua lógica de processamento gere uma exceção. Bloqueios não liberados impedem outros processos enquanto a concessão não expira.

Também é possível implementar um mecanismo de bloqueio simples sem a biblioteca de clientes de bloqueio usando gravações condicionais diretamente. O exemplo a seguir usa UpdateItem com uma expressão de condição para adquirir um bloqueio e DeleteItem para liberá-lo:

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

Essa abordagem usa uma expressão de condição para garantir que um bloqueio só possa ser adquirido se ainda não existir ou tiver expirado e só possa ser liberado pelo processo que o adquiriu. Considere a possibilidade de habilitar a vida útil (TTL) na tabela de bloqueio para limpar automaticamente os itens de bloqueio expirados.