

# Práticas recomendadas para lidar com atualizações simultâneas no DynamoDB
<a name="BestPractices_ImplementingVersionControl"></a>

Em sistemas distribuídos, vários processos ou usuários podem tentar modificar os mesmos dados ao mesmo tempo. Sem o controle de simultaneidade, essas gravações simultâneas podem resultar em perda de atualizações, dados inconsistentes ou condições de disputa. O DynamoDB disponibiliza vários mecanismos para ajudar a gerenciar o acesso simultâneo e manter a integridade dos dados.

**nota**  
Operações de gravação individuais, como `UpdateItem`, são atômicas e sempre operam na versão mais recente do item, independentemente da simultaneidade. As estratégias de bloqueio são necessárias quando a aplicação precisa ler um item e depois gravá-lo novamente com base no valor de leitura (um ciclo de leitura, modificação e gravação), pois outro processo pode modificar o item entre a leitura e a gravação.

Há duas estratégias principais para lidar com atualizações simultâneas:
+ **Bloqueio positivo**: presume que os conflitos são raros. Ele proporciona acesso simultâneo e detecta conflitos no momento da gravação usando gravações condicionais. Se um conflito for detectado, a gravação falhará e a aplicação poderá tentar novamente.
+ **Bloqueio negativo**: presume que existe a probabilidade de conflitos. Ele impede o acesso simultâneo adquirindo acesso exclusivo a um recurso antes de modificá-lo. Os demais processos devem esperar até que o bloqueio seja liberado.

A seguinte tabela resume as abordagens disponíveis no DynamoDB:


| Abordagem | Mecanismo | Melhor para | 
| --- | --- | --- | 
| Bloqueio otimista | Atributo da versão \$1 gravações condicionais | Baixa contenção, novas tentativas de baixo custo | 
| Bloqueio negativo (transações) | TransactWriteItems | Atomicidade de vários itens, contenção moderada | 
| Bloqueio negativo (cliente de bloqueio) | Tabela de bloqueio dedicada com concessão e heartbeat | Fluxos de trabalho de longa duração, coordenação distribuída | 

# Bloqueio positivo com número de versão
<a name="BestPractices_OptimisticLocking"></a>

O bloqueio positivo é uma estratégia que detecta conflitos no momento da gravação, em vez de evitá-los. Cada item inclui um atributo de versão que aumenta a cada atualização. Ao atualizar um item, é necessário incluir uma [expressão de condição](Expressions.ConditionExpressions.md) que verifica se o número da versão corresponde ao valor que a aplicação leu pela última vez. Se outro processo modificou o item nesse meio tempo, a condição falhará e o DynamoDB exibirá uma `ConditionalCheckFailedException`.

## Quando usar o bloqueio positivo
<a name="BestPractices_OptimisticLocking_WhenToUse"></a>

O bloqueio positivo é uma boa opção quando:
+ Vários usuários ou processos podem tentar atualizar o mesmo item, mas os conflitos são infrequentes.
+ O custo de tentar novamente uma gravação é baixo para a aplicação.
+ Você deseja evitar os custos indiretos e a complexidade do gerenciamento de bloqueios distribuídos.

Os exemplos comuns incluem atualizações de inventário de comércio eletrônico, plataformas de edição colaborativa e registros de transações financeiras.

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

**Custos indiretos de novas tentativas em alta contenção**  
Em ambientes de alta simultaneidade, a probabilidade de conflitos aumenta, podendo gerar mais tentativas e custos de gravação.

**Complexidade de implementação**  
Adicionar controle de versão aos itens e lidar com verificações condicionais pode aumentar complexidade da lógica da aplicação. O AWS SDK para Java v2 Enhanced Client oferece suporte integrado por meio da anotação [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 gerencia automaticamente os números de versão para você.

## Padrão de design
<a name="BestPractices_OptimisticLocking_PatternDesign"></a>

Inclua um atributo de versão em cada item. Aqui está um design de esquema simples:
+ Chave de partição: um identificador exclusivo para cada item (por exemplo, `ItemId`).
+ Atributos:
  + `ItemId`: o identificador exclusivo do item.
  + `Version`: um número inteiro que representa o número da versão do item.
  + `QuantityLeft`: o estoque restante do item.

Quando um item é criado pela primeira vez, o atributo `Version` é definido como 1. Com cada atualização, o número da versão é incrementado em 1.


| ItemID (chave de partição) | Versão | QuantityLeft | 
| --- | --- | --- | 
| Bananas | 1 | 10 | 
| Maçãs | 1 | 5 | 
| Laranjas | 1 | 7 | 

## Implementação
<a name="BestPractices_OptimisticLocking_Implementation"></a>

Para implementar o bloqueio positivo, siga estas etapas:

1. Leia a versão atual do item.

   ```
   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. Atualize o item usando uma expressão de condição que verifica o número da versão.

   ```
   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. Resolva os conflitos tentando novamente com uma nova leitura.

   Como cada nova tentativa requer uma leitura adicional, limite o número total de novas tentativas.

   ```
   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.")
   ```

Em aplicações Java, o AWS SDK para Java v2 Enhanced Client oferece suporte integrado a bloqueio positivo por meio da anotação[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 gerencia automaticamente os números de versão para você.

Para ter mais informações sobre expressões de condição, consulte [Exemplo de expressão de condição do DynamoDB na CLI](Expressions.ConditionExpressions.md).

# Bloqueio negativo com transações do DynamoDB
<a name="BestPractices_PessimisticLocking"></a>

As transações do [DynamoDB](transactions.md) oferecem uma abordagem de tudo ou nada para operações agrupadas. Quando você usa `TransactWriteItems`, o DynamoDB monitora todos os itens na transação. Se algum item for modificado por outra operação durante a transação, toda a transação será cancelada e o DynamoDB exibirá uma `TransactionCanceledException`. Esse comportamento oferece uma forma de controle de simultaneidade negativo porque modificações simultâneas conflitantes são evitadas em vez de detectadas assim que ocorrem.

## Quando usar transações para bloqueio
<a name="BestPractices_PessimisticLocking_WhenToUse"></a>

As transações são uma boa opção quando:
+ É necessário atualizar vários itens atomicamente, dentro da mesma tabela ou entre tabelas.
+ A lógica de negócios exige uma semântica de tudo ou nada: ou todas as alterações têm êxito ou nenhuma é aplicada.

Exemplos comuns incluem transferir fundos entre contas, fazer pedidos que atualizem o inventário e as tabelas de pedidos e trocar itens entre jogadores em um jogo.

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

**Custo de gravação mais alto**  
Para itens de até 1 KB, as transações consomem 2 WCUs por item (uma para preparar e outra para confirmar), em comparação com 1 WCU para uma gravação padrão.

**Limite de itens**  
Uma única transação pode incluir até cem ações em uma ou mais tabelas.

**Sensibilidade a conflitos**  
Se algum item for modificado por outra operação, toda a transação falhará. Em cenários de alta contenção, isso pode levar a cancelamentos frequentes.

## Implementação
<a name="BestPractices_PessimisticLocking_Implementation"></a>

O exemplo a seguir usa `TransactWriteItems` para transferir inventário entre dois itens atomicamente. Se outro processo modificar qualquer item durante a transação, toda a operação será revertida.

```
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
```

Neste exemplo, a expressão de condição verifica se existe inventário suficiente, mas nenhum atributo de versão é necessário. O DynamoDB cancelará automaticamente a transação se algum item da transação for modificado por outra operação entre as fases de preparação e confirmação. É isso que oferece o controle de simultaneidade negativo: modificações simultâneas conflitantes são evitadas pela própria transação.

**nota**  
É possível associar transações com bloqueio positivo adicionando verificações de versão como expressões de condição adicionais. Isso oferece um nível extra de proteção, mas não é necessário para que a transação detecte conflitos.

Para obter mais informações, consulte [Gerenciar fluxos de trabalho complexos com transações do DynamoDB](transactions.md).

# Bloqueio distribuído com o DynamoDB Lock Client
<a name="BestPractices_DistributedLocking"></a>

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

## Como funciona
<a name="BestPractices_DistributedLocking_HowItWorks"></a>

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

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

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

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)](TTL.md) na tabela de bloqueio para limpar automaticamente os itens de bloqueio expirados.

## Escolher uma estratégia de controle de simultaneidade
<a name="BestPractices_ChoosingLockingStrategy"></a>

Siga as diretrizes abaixo para escolher a abordagem ideal para sua workload:

**Use o bloqueio positivo** quando:  
+ Os conflitos forem infrequentes.
+ O custo de tentar novamente uma gravação for baixo para a aplicação.
+ Você estiver atualizando um item de cada vez.

**Use transações** quando:  
+ For necessário atualizar vários itens atomicamente.
+ Você precisar da semântica tudo ou nada em itens ou tabelas.
+ For necessário associar verificações de condição e gravações em uma única operação.

**Use o cliente de bloqueio** quando:  
+ For necessário coordenar o acesso a recursos externos em todos os processos distribuídos.
+ A seção essencial for de longa duração e tentar novamente em caso de conflito for caro.
+ Você precisar da expiração automática de bloqueio para lidar com as falhas do processo normalmente.

**nota**  
Se você usa [tabelas globais do DynamoDB](GlobalTables.md), esteja ciente de que as tabelas globais usam uma estratégia de reconciliação do tipo “último gravador prevalece” para atualizações simultâneas. O bloqueio positivo com números de versão não funciona conforme o esperado em todas as regiões porque uma gravação em uma região pode sobrescrever uma gravação simultânea em outra região sem uma verificação de versão. Projete sua aplicação para lidar com conflitos em nível de aplicação ao usar tabelas globais.