Bloqueo distribuido con el cliente de bloqueo de DynamoDB
Para las aplicaciones que requieren la semántica tradicional de bloqueo, adquisición y liberación, el cliente de bloqueo de DynamoDB es una biblioteca de código abierto que implementa el bloqueo distribuido mediante una tabla de DynamoDB como almacén de bloqueos. Este enfoque resulta útil cuando se necesita coordinar el acceso a un recurso externo (como un objeto de S3 o una configuración compartida) en varias instancias de aplicaciones.
El cliente de bloqueo está disponible como una biblioteca Java
Funcionamiento
El cliente de bloqueo utiliza una tabla de DynamoDB dedicada para realizar un seguimiento de los bloqueos. Cada bloqueo se representa como un elemento con los siguientes atributos de claves:
Una clave de partición que identifica el recurso que se está bloqueando.
La duración de una asignación que especifica cuánto tiempo es válido el bloqueo. Si el titular del bloqueo se bloquea o deja de responder, el bloqueo caduca automáticamente una vez finalizada la asignación.
Un latido que el titular del bloqueo envía periódicamente para extender la asignación. Esto evita que el bloqueo caduque mientras el titular lo esté procesando activamente.
El cliente de bloqueo utiliza escrituras condicionales para garantizar que solo un proceso pueda adquirir un bloqueo a la vez. Si ya se ha mantenido un bloqueo, la persona que llama puede optar por esperar y volver a intentarlo o que se produzca un error inmediatamente.
Cuándo usar el cliente de bloqueo
El cliente de bloqueo es una buena opción cuando:
Debe coordinar el acceso a un recurso compartido entre varias instancias de aplicaciones o microservicios.
La parte crítica es de larga duración (de segundos a minutos) y volver a intentar toda la operación en caso de conflicto resultaría caro.
Es necesario que el bloqueo caduque automáticamente para gestionar correctamente los errores del proceso.
Algunos ejemplos comunes incluyen la orquestación de flujos de trabajo distribuidos, la coordinación de trabajos cron en varias instancias y la administración del acceso a recursos externos compartidos.
Desventajas
- Infraestructura adicional
Requiere una tabla de DynamoDB dedicada para la administración de bloqueos, con capacidad adicional de lectura y escritura para las operaciones de bloqueo y los latidos.
- Dependencia del reloj
La caducidad del bloqueo depende de las marcas temporales. Un sesgo horario significativo entre los clientes puede provocar un comportamiento inesperado, especialmente en el caso de asignaciones de corta duración.
- Riesgo de bloqueo
Si la aplicación se bloquea en varios recursos, debe adquirirlos en un orden coherente para evitar bloqueos. La duración de la asignación proporciona una red de seguridad al liberar automáticamente los bloqueos de los titulares que no respondan.
Implementación
En el siguiente ejemplo, se muestra cómo utilizar el cliente de bloqueo de DynamoDB para adquirir y liberar un bloqueo:
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
Libere siempre los bloqueos de un bloqueo finally para garantizar que se liberen incluso si la lógica de procesamiento genera una excepción. Los bloqueos no liberados bloquean otros procesos hasta que venza la asignación.
También puede implementar un mecanismo de bloqueo simple sin la biblioteca de cliente de bloqueo mediante el uso directo de escrituras condicionales. En el siguiente ejemplo, se utiliza UpdateItem con una expresión de condición para adquirir un bloqueo y DeleteItem para liberarlo:
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
Este enfoque utiliza una expresión de condición para garantizar que un bloqueo solo se pueda adquirir si no existe o ha caducado, y solo se puede liberar mediante el proceso que lo adquirió. Considere la posibilidad de activar Periodo de vida (TTL) en la tabla de bloqueos para limpiar automáticamente los elementos de bloqueo caducados.