

# Prácticas recomendadas para gestionar las actualizaciones simultáneas en DynamoDB
<a name="BestPractices_ImplementingVersionControl"></a>

En sistemas distribuidos, varios procesos o usuarios pueden intentar modificar los mismos datos al mismo tiempo. Sin un control de simultaneidad, estas escrituras simultáneas pueden provocar la pérdida de actualizaciones, datos incoherentes o condiciones de carrera. DynamoDB proporciona varios mecanismos como ayuda para administrar el acceso simultáneo y mantener la integridad de los datos.

**nota**  
Las operaciones de escritura individuales, como `UpdateItem`, son atómicas y siempre funcionan en la versión más reciente del elemento, independientemente de la simultaneidad. Las estrategias de bloqueo son necesarias cuando la aplicación debe leer un elemento y volver a escribirlo en función del valor de lectura (un ciclo de lectura-modificación-escritura), ya que otro proceso podría modificar el elemento entre la lectura y la escritura.

Existen dos estrategias principales para gestionar las actualizaciones simultáneas:
+ **Bloqueo positivo**: supone que los conflictos son poco frecuentes. Permite el acceso simultáneo y detecta conflictos en tiempo de escritura mediante escrituras condicionales. Si se detecta un conflicto, se produce un error en la escritura y la aplicación puede volver a intentarlo.
+ **Bloqueo negativo**: supone que es probable que haya conflictos. Impide el acceso simultáneo al adquirir acceso exclusivo a un recurso antes de modificarlo. Los demás procesos deben esperar hasta que se libere el bloqueo.

En la siguiente tabla, se resumen los enfoques disponibles en DynamoDB:


| Enfoque | Mecanismo | Lo mejor para | 
| --- | --- | --- | 
| Bloqueo optimista | Atributo de versión \$1 escrituras condicionales | Baja contención, reintentos económicos | 
| Bloqueo negativo (transacciones) | TransactWriteItems | Atomicidad de varios elementos, contención moderada | 
| Bloqueo negativo (cliente de bloqueo) | Tabla de bloqueo exclusiva con asignación y latido | Flujos de trabajo prolongados, coordinación distribuida | 

# Bloqueo positivo con el número de versión
<a name="BestPractices_OptimisticLocking"></a>

El bloqueo positivo es una estrategia que detecta los conflictos en el momento de la escritura en lugar de prevenirlos. Cada elemento incluye un atributo de versión que se incrementa con cada actualización. Al actualizar un elemento, se incluye una [expresión de condición](Expressions.ConditionExpressions.md) que comprueba si el número de versión coincide con el valor que leyó la aplicación por última vez. Si otro proceso ha modificado el elemento mientras tanto, la condición produce un error y DynamoDB devuelve una `ConditionalCheckFailedException`.

## Cuándo utilizar el bloqueo positivo
<a name="BestPractices_OptimisticLocking_WhenToUse"></a>

El bloqueo positivo es una buena opción cuando:
+ Varios usuarios o procesos pueden actualizar el mismo elemento, pero los conflictos no son frecuentes.
+ Reintentar una escritura errónea no es costoso para la aplicación.
+ Es necesario evitar la sobrecarga y la complejidad de administrar bloqueos distribuidos.

Los ejemplos más comunes incluyen las actualizaciones del inventario del comercio electrónico, las plataformas de edición colaborativa y los registros de transacciones financieras.

## Desventajas
<a name="BestPractices_OptimisticLocking_Tradeoffs"></a>

**Sobrecarga de reintentos en alta contención**  
En entornos de alta simultaneidad, aumenta la probabilidad de conflictos, lo que puede provocar un mayor número de reintentos y mayores costos de escritura.

**Complejidad de la implementación**  
Agregar control de versiones a elementos y controlar condiciones agrega complejidad a la lógica de la aplicación. El cliente mejorado de AWS SDK para Java v2 ofrece soporte integrado a través de la anotación [https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE), que administra automáticamente los números de versión.

## Diseño de patrón
<a name="BestPractices_OptimisticLocking_PatternDesign"></a>

Incluya un atributo de versión en cada elemento. A continuación, mostramos un esquema sencillo:
+ Clave de partición: un identificador único para cada elemento (por ejemplo, `ItemId`).
+ Atributos:
  + `ItemId`: el identificador único del elemento.
  + `Version`: un número entero que representa el número de versión del elemento.
  + `QuantityLeft`: el inventario restante del elemento.

Cuando se crea un elemento por primera vez, el atributo `Version` se establece en 1. Con cada actualización, el número de versión aumenta en uno.


| ItemID (clave de partición) | Versión | QuantityLeft | 
| --- | --- | --- | 
| Bananas | 1 | 10 | 
| Manzanas | 1 | 5 | 
| Naranjas | 1 | 7 | 

## Implementación
<a name="BestPractices_OptimisticLocking_Implementation"></a>

Para implementar el bloqueo positivo, siga estos pasos:

1. Lea la versión actual del elemento.

   ```
   def get_item(item_id):
       response = table.get_item(Key={'ItemID': item_id})
       return response['Item']
   
   item = get_item('Bananas')
   current_version = item['Version']
   ```

1. Actualice el elemento mediante una expresión de condición que compruebe el número de versión.

   ```
   def update_item(item_id, qty_bought, current_version):
       try:
           response = table.update_item(
               Key={'ItemID': item_id},
               UpdateExpression="SET QuantityLeft = QuantityLeft - :qty, Version = :new_v",
               ConditionExpression="Version = :expected_v",
               ExpressionAttributeValues={
                   ':qty': qty_bought,
                   ':new_v': current_version + 1,
                   ':expected_v': current_version
               },
               ReturnValues="UPDATED_NEW"
           )
           return response
       except ClientError as e:
           if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
               print("Version conflict: another process updated this item.")
           raise
   ```

1. Controle los conflictos volviendo a intentarlo con una lectura nueva.

   Cada reintento requiere una lectura adicional, así que limite el número total de reintentos.

   ```
   def update_with_retry(item_id, qty_bought, max_retries=3):
       for attempt in range(max_retries):
           item = get_item(item_id)
           try:
               return update_item(item_id, qty_bought, item['Version'])
           except ClientError as e:
               if e.response['Error']['Code'] != 'ConditionalCheckFailedException':
                   raise
               print(f"Retry {attempt + 1}/{max_retries}")
       raise Exception("Update failed after maximum retries.")
   ```

Para aplicaciones de Java, el cliente mejorado de AWS SDK para Java v2 ofrece soporte de bloqueo positivo integrado a través de la anotación [https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/ddb-en-client-extensions.html#ddb-en-client-extensions-VRE), que administra automáticamente los números de versión.

Para obtener más información sobre las expresiones de condición, consulte [Ejemplo de la CLI de expresión de condición de DynamoDB](Expressions.ConditionExpressions.md).

# Bloqueo negativo con las transacciones de DynamoDB
<a name="BestPractices_PessimisticLocking"></a>

Las [transacciones](transactions.md) de DynamoDB ofrecen un enfoque de todo o nada para las operaciones agrupadas. Cuando usa `TransactWriteItems`, DynamoDB supervisa todos los artículos de la transacción. Si alguna operación modifica algún elemento durante la transacción, se cancela toda la transacción y DynamoDB devuelve una `TransactionCanceledException`. Este comportamiento proporciona una forma de control de la simultaneidad negativo, ya que las modificaciones simultáneas conflictivas se previenen en lugar de detectarse a posteriori.

## Cuándo utilizar las transacciones para el bloqueo
<a name="BestPractices_PessimisticLocking_WhenToUse"></a>

Las transacciones son una buena opción cuando:
+ Debe actualizar varios elementos de forma atómica, ya sea dentro de la misma tabla o en varias tablas.
+ La lógica empresarial requiere una semántica de todo o nada, o bien todos los cambios se realizan correctamente o no se aplica ninguno.

Algunos ejemplos habituales son la transferencia de fondos entre cuentas, la realización de pedidos que actualizan el inventario y las tablas de pedidos y el intercambio de elementos entre los jugadores de un juego.

## Desventajas
<a name="BestPractices_PessimisticLocking_Tradeoffs"></a>

**Mayor costo de escritura**  
Para los elementos de hasta 1 KB, las transacciones consumen 2 WCU por elemento (una para preparar y otra para confirmar), en comparación con 1 WCU para una escritura estándar.

**Límite de elementos**  
Una transacción puede incluir hasta 100 acciones en una o varias tablas.

**Sensibilidad a los conflictos**  
Si otra operación modifica algún elemento de la transacción, toda la transacción produce un error. En situaciones de alta contención, esto puede provocar cancelaciones frecuentes.

## Implementación
<a name="BestPractices_PessimisticLocking_Implementation"></a>

En el siguiente ejemplo, se utiliza `TransactWriteItems` para transferir el inventario entre dos elementos de forma atómica. Si otro proceso modifica alguno de los elementos durante la transacción, se anula toda la operación.

```
import boto3

client = boto3.client('dynamodb')

def transfer_inventory(source_id, target_id, quantity):
    try:
        client.transact_write_items(
            TransactItems=[
                {
                    'Update': {
                        'TableName': 'Inventory',
                        'Key': {'ItemID': {'S': source_id}},
                        'UpdateExpression': 'SET QuantityLeft = QuantityLeft - :qty',
                        'ConditionExpression': 'QuantityLeft >= :qty',
                        'ExpressionAttributeValues': {
                            ':qty': {'N': str(quantity)}
                        }
                    }
                },
                {
                    'Update': {
                        'TableName': 'Inventory',
                        'Key': {'ItemID': {'S': target_id}},
                        'UpdateExpression': 'SET QuantityLeft = QuantityLeft + :qty',
                        'ExpressionAttributeValues': {
                            ':qty': {'N': str(quantity)}
                        }
                    }
                }
            ]
        )
        return True
    except client.exceptions.TransactionCanceledException as e:
        print(f"Transaction canceled: {e}")
        return False
```

En este ejemplo, la expresión de condición comprueba que existe suficiente inventario, pero no se necesita ningún atributo de versión. DynamoDB cancela automáticamente la transacción si otra operación modifica algún elemento de la transacción entre las fases de preparación y confirmación. Esto es lo que proporciona el control de simultaneidad negativo, la propia transacción evita las modificaciones simultáneas conflictivas.

**nota**  
Puede combinar transacciones con un bloqueo positivo agregando verificaciones de versión como expresiones de condición adicionales. Esto proporciona una capa adicional de protección, pero no es necesario para que la transacción detecte conflictos.

Para obtener más información, consulte [Administración de flujos de trabajo complejos con transacciones de DynamoDB](transactions.md).

# Bloqueo distribuido con el cliente de bloqueo de DynamoDB
<a name="BestPractices_DistributedLocking"></a>

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](https://github.com/awslabs/amazon-dynamodb-lock-client) de código abierto.

## Funcionamiento
<a name="BestPractices_DistributedLocking_HowItWorks"></a>

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
<a name="BestPractices_DistributedLocking_WhenToUse"></a>

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
<a name="BestPractices_DistributedLocking_Tradeoffs"></a>

**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
<a name="BestPractices_DistributedLocking_Implementation"></a>

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)](TTL.md) en la tabla de bloqueos para limpiar automáticamente los elementos de bloqueo caducados.

## Elección de una estrategia de control de simultaneidad
<a name="BestPractices_ChoosingLockingStrategy"></a>

Use las siguientes directrices para elegir el enfoque adecuado para satisfacer su carga de trabajo:

**Use el bloqueo positivo** cuando:  
+ Los conflictos son poco frecuentes.
+ El reintento de una escritura errónea no es caro.
+ Actualice un elemento a la vez.

**Use las transacciones** cuando:  
+ Necesita actualizar varios elementos de forma atómica.
+ Se requiere una semántica de todo o nada en todos los elementos o tablas.
+ Debe combinar las comprobaciones de condición con las escrituras en una sola operación.

**Use el bloqueo de cliente** cuando:  
+ Debe coordinar el acceso a los recursos externos en todos los procesos distribuidos.
+ La sección crítica es de larga duración y volver a intentarlo en caso de conflicto resulta caro.
+ Es necesario que el bloqueo caduque automáticamente para gestionar los errores del proceso.

**nota**  
Si utiliza [tablas globales de DynamoDB](GlobalTables.md), tenga en cuenta que las tablas globales utilizan una estrategia de reconciliación según la cual “el último escritor gana” para las actualizaciones simultáneas. El bloqueo positivo con números de versión no funciona según lo esperado en todas las regiones, ya que una escritura en una región puede sobrescribir una escritura simultánea en otra región sin comprobar la versión. Diseñe su aplicación para gestionar los conflictos por aplicación cuando utilice tablas globales.