DEV Community ūüĎ©‚ÄćūüíĽūüĎ®‚ÄćūüíĽ

Cover image for Amazon DynamoDB: Casos de Uso e Exemplos com Transactions
Eduardo Rabelo
Eduardo Rabelo

Posted on

Amazon DynamoDB: Casos de Uso e Exemplos com Transactions

O DynamoDB da Amazon foi lan√ßado em 2012 e vem adicionando uma s√©rie de novos recursos desde ent√£o. √Č dif√≠cil de acreditar agora, mas a vers√£o original do DynamoDB n√£o tinha streams, scan paralelos ou √≠ndices secund√°rios.

Um dos lan√ßamentos de recursos mais interessantes do DynamoDB nos √ļltimos anos foi a adi√ß√£o de Transa√ß√Ķes do DynamoDB no re:Invent 2018. Com as Transa√ß√Ķes do DynamoDB, voc√™ pode escrever ou ler um lote de itens do DynamoDB e toda a solicita√ß√£o ser√° bem-sucedida ou falhar√° em conjunto.

Esta vers√£o do recurso simplificou muitos fluxos de trabalho que envolviam versionamento complexos e v√°rias solicita√ß√Ķes para se trabalhar com precis√£o em v√°rios itens. Nesta postagem, analisaremos como e por que usar as transa√ß√Ķes do DynamoDB.

Nós cobriremos:

  • Informa√ß√Ķes b√°sicas sobre transa√ß√Ķes do DynamoDB, incluindo diferen√ßas com opera√ß√Ķes em lote (batch) e idempot√™ncia nas transa√ß√Ķes
  • Tr√™s casos de uso comuns para transa√ß√Ķes do DynamoDB

Em um pr√≥ximo artigo, veremos algumas an√°lises de desempenho para ver quanto tempo as solicita√ß√Ķes de transa√ß√£o do DynamoDB levam em compara√ß√£o com as a√ß√Ķes padr√£o de item √ļnico e at√© mesmo com a√ß√Ķes em lote.

Vamos começar!

Informa√ß√Ķes b√°sicas sobre transa√ß√Ķes do DynamoDB

Para come√ßar, alguns detalhes sobre as transa√ß√Ķes do DynamoDB. Abordaremos duas √°reas:

  1. Quais s√£o as APIs transacionais e como elas diferem das APIs em lote?
  2. Manipulando idempot√™ncia com solicita√ß√Ķes transacionais.

Quais s√£o as APIs transacionais?

Existem duas chamadas de API que lidam com transa√ß√Ķes - TransactWriteItems e TransactGetItems. Como voc√™ pode adivinhar pelo nome, o primeiro √© usado para escrever v√°rios itens em uma √ļnica transa√ß√£o. O segundo √© usado para ler v√°rios itens em uma √ļnica transa√ß√£o. Ambas as APIs transacionais permitem operar em at√© 25 itens em uma √ļnica solicita√ß√£o.

O DynamoDB há muito tempo tem APIs baseadas em lote que operam em vários itens ao mesmo tempo. Você pode BatchGetItem para ler até 100 itens de uma só vez ou BatchWriteItem para escrever até 25 itens de uma só vez.

Existem duas diferen√ßas principais entre as APIs Batch* e as APIs Transact*. O primeiro √© sobre o consumo de capacidade. Ao usar as APIs Transact*, voc√™ ser√° cobrado duas vezes a capacidade que seria consumida se executasse as opera√ß√Ķes sem uma transa√ß√£o. Portanto, se voc√™ tiver uma solicita√ß√£o TransactWriteItem` que insira dois itens com menos de 1KB, voc√™ ser√° cobrado por 4 unidades de capacidade de grava√ß√£o - 2 itens de 1KB X 2 para transa√ß√Ķes.

A segunda diferen√ßa entre as APIs Transact* e as APIs Batch* est√° relacionada aos modos de falha. Com as APIs Transact*, todas as leituras ou grava√ß√Ķes ter√£o √™xito ou falhar√£o juntas . Nas APIs Batch*, algumas solicita√ß√Ķes podem ser bem-sucedidas e outras podem falhar, e voc√™ decide os erros.

Uma solicita√ß√£o transacional pode falhar por v√°rios motivos. Primeiro, um dos elementos em uma solicita√ß√£o pode falhar devido √†s condi√ß√Ķes na solicita√ß√£o. Para qualquer uma das solicita√ß√Ķes baseadas em grava√ß√£o, voc√™ pode incluir uma express√£o de condi√ß√£o na solicita√ß√£o. Se essas condi√ß√Ķes n√£o forem atendidas, a grava√ß√£o falhar√° e todo o lote falhar√°.

Segundo, uma transa√ß√£o pode falhar se um dos itens estiver sendo alterado em uma transa√ß√£o ou solicita√ß√£o separada. Por exemplo, se voc√™ fizer uma solicita√ß√£o TransactGetItems em um item ao mesmo tempo em que houver uma solicita√ß√£o TransactWriteItems em aberto sendo processada no item, a solicita√ß√£o TransactGetItems falhar√°. Esse tipo de falha √© conhecido como conflito de transa√ß√£o e √© poss√≠vel visualizar no CloudWatch Metrics o n√ļmero de conflitos de transa√ß√£o em suas tabelas.

Por fim, uma transação pode falhar por outros motivos, como sua tabela não ter capacidade suficiente ou o serviço DynamoDB sendo desativado em geral.

Idempot√™ncia com solicita√ß√Ķes transacionais

Para a API TransactWriteItem, o DynamoDB permite que voc√™ transmita um par√Ęmetro ClientRequestToken com sua solicita√ß√£o. A inclus√£o desse par√Ęmetro permitir√° garantir que sua solicita√ß√£o seja idempotente, mesmo que enviada v√°rias vezes.

Para ver como isso √© √ļtil, imagine que voc√™ esteja fazendo uma solicita√ß√£o TransactWriteItem que inclui algumas solicita√ß√Ķes de grava√ß√£o para incrementar um atributo em um item. Se voc√™ teve um problema de rede em que n√£o sabia se essa opera√ß√£o teve √™xito ou falhou, voc√™ pode estar em um estado ruim. Se voc√™ assumir que a opera√ß√£o foi bem-sucedida, mas n√£o obteve, o valor do seu atributo ser√° menor do que deveria. Se voc√™ presumir que a opera√ß√£o falhou quando n√£o ocorreu, poder√° enviar a solicita√ß√£o novamente, mas o valor do atributo ser√° maior do que deveria.

O ClientRequestToken lida com isso. Se voc√™ enviar uma solicita√ß√£o com o mesmo token e os mesmos par√Ęmetros em um per√≠odo de 10 minutos, o DynamoDB garantir√° que a solicita√ß√£o seja idempotente. Se a solicita√ß√£o for bem-sucedida quando enviada inicialmente, o DynamoDB n√£o a executar√° novamente. Se falhou na primeira vez, o DynamoDB aplicar√° as grava√ß√Ķes na solicita√ß√£o.

A API TransactWriteItems √© a √ļnica API do DynamoDB que permite a idempot√™ncia; portanto, voc√™ pode usar TransactWriteItems mesmo com um √ļnico item se tiver uma forte necessidade de idempot√™ncia.

Casos de uso comuns para transa√ß√Ķes do DynamoDB

Agora que conhecemos os conceitos b√°sicos sobre transa√ß√Ķes do DynamoDB, vamos v√™-los em a√ß√£o. Lembre-se de que as Transa√ß√Ķes do DynamoDB custam o dobro de uma opera√ß√£o semelhante sem transa√ß√£o; portanto, devemos ser criteriosos e usar transa√ß√Ķes somente quando realmente precisamos delas.

Quando s√£o bons momentos para usar transa√ß√Ķes? Eu tenho tr√™s exemplos favoritos que abordaremos abaixo:

  • Mantendo a exclusividade em v√°rios atributos
  • Manipula√ß√£o de contagens e preven√ß√£o de duplicatas
  • Autorizando um usu√°rio a executar uma determinada a√ß√£o

Não incluí um exemplo em que eu precisaria de idempotência, conforme discutido na seção anterior, mas esse é outro exemplo de um bom caso de uso para as APIs transacionais.

Vamos revisar cada um dos exemplos por vez.

Mantendo a exclusividade em v√°rios atributos

No DynamoDB, se você quiser garantir que um atributo específico seja exclusivo, precisará criar esse atributo diretamente na chave primária.

Um exemplo fácil aqui é um fluxo de inscrição do usuário para um aplicativo. Você deseja que o nome de usuário seja exclusivo em seu aplicativo, então você criar uma chave primária que inclua o nome de usuário.

Na tabela acima, nossos valores de PK e SK incluem os valores username para que eles sejam √ļnicos.

Mas e se você também quiser garantir que um determinado endereço de email seja exclusivo em todo o sistema, para que não haja pessoas que se inscrevam em várias contas no mesmo endereço de email?

Você também pode adicionar email à sua chave primária:

Agora, nossa tabela inclui o nome de usu√°rio no PK e o email no SK.

No entanto, isso n√£o vai funcionar. √Č a combina√ß√£o de uma chave de parti√ß√£o e uma chave de classifica√ß√£o que torna um item exclusivo dentro da tabela. Usando essa estrutura de chaves, voc√™ confirma que um endere√ßo de email ser√° usado apenas uma vez para esse nome de usu√°rio. Agora voc√™ perdeu as propriedades originais de exclusividade no nome de usu√°rio, pois outra pessoa pode se inscrever com o mesmo nome de usu√°rio e um endere√ßo de email diferente!

Se você deseja garantir que um nome de usuário e um endereço de email sejam exclusivos em sua tabela, é necessário criar um item para cada um e adicionar esses itens a uma transação.

O código para escrever essa transação seria o seguinte:

python
response = client.transact_write_items(
TransactItems=[
{
'Put': {
'TableName': 'UsersTable',
'Item': {
'PK': { 'S': 'USER#alexdebrie' },
'SK': { 'S': 'USER#alexdebrie' },
'Username': { 'S': 'alexdebrie' },
'FirstName': { 'S': 'Alex' },
...
},
'ConditionExpression': 'attribute_not_exists(PK)'
}
},
{
'Put': {
'TableName': 'UsersTable',
'Item': {
'PK': { 'S': 'USEREMAIL#alex@debrie.com' },
'SK': { 'S': 'USEREMAIL#alex@debrie.com' },
},
'ConditionExpression': 'attribute_not_exists(PK)'
}
}
]
)

E agora sua tabela teria a seguinte aparência:

Observe que o item que armazena um usuário por endereço de email não possui nenhuma propriedade do usuário. Você pode fazer isso se planeja dar acesso por um nome de usuário e nunca por um endereço de email. O item do endereço de email é criado apenas como um marcador que rastreia se o email foi usado.

Se voc√™ vai acessar um usu√°rio pelo endere√ßo de e-mail, ent√£o voc√™ precisa duplicar todas as informa√ß√Ķes em ambos os itens. Em seguida, sua tabela pode ter a seguinte apar√™ncia:

Eu evitaria isso, se poss√≠vel. Agora, toda atualiza√ß√£o no item do usu√°rio precisa ser uma transa√ß√£o para atualizar os dois itens. Isso aumentar√° o custo de suas grava√ß√Ķes e a lat√™ncia em suas solicita√ß√Ķes.

Manipulando contagens e prevenção de duplicatas

Um segundo lugar em que as transa√ß√Ķes podem ser √ļteis √© armazenar contagens para itens relacionados. Vamos ver como isso funciona.

Imagine que você tenha um aplicativo social com algum tipo de sistema para 'gostar' de itens. Pode ser o Twitter, como os usuários gostam de outros tweets. Pode ser o Reddit, onde os usuários podem curtir ou votar em postagens ou comentários específicos. Ou pode ser o GitHub, onde um usuário pode adicionar uma reação a um problema.

Em todas essas situa√ß√Ķes, conv√©m armazenar um registro do usu√°rio que est√° votando positivamente em um item espec√≠fico para garantir que o usu√°rio n√£o vote v√°rias vezes.

Al√©m disso, ao exibir o item que pode ser votado, voc√™ deseja exibir o n√ļmero total de votos. √Č mais eficiente desnormalizar isso armazenando uma propriedade de contador no pr√≥prio item, em vez de fazer uma opera√ß√£o de Consulta toda vez para buscar todos os itens que indicam que o item foi votado.

Sua tabela pode ter a seguinte aparência:

Esta tabela √© uma vers√£o simplificada do Reddit. Os usu√°rios podem criar Postagens, e outros usu√°rios podem gostar das Postagens. Nesta tabela, temos 5 itens. Dois deles s√£o itens de postagem (usando o POST<PostId> padr√£o para PK e SK), enquanto tr√™s deles s√£o itens semelhantes a usu√°rios (usando o mesmo POST#<PostId> padr√£o para PK e um USER#<Username> padr√£o para SK). Observe como cada item de postagem possui um atributo UpvotesCount que armazena o n√ļmero total de upvotes recebidos.

Quando um usu√°rio faz uma vota√ß√£o de um item, primeiro voc√™ deseja garantir que o usu√°rio n√£o tenha votado anteriormente no item e depois incrementa o atributo UpvotesCount no item. Em um mundo sem transa√ß√Ķes, esse seria um processo de duas etapas.

Com transa√ß√Ķes, voc√™ pode fazer isso em uma √ļnica etapa. O c√≥digo para executar esta transa√ß√£o seria o seguinte:

python
response = client.transact_write_items(
TransactItems=[
{
'Put': {
'TableName': 'RedditTable',
'Item': {
'PK': { 'S': 'POST#1caa5be06389' },
'SK': { 'S': 'USER#alexdebrie' },
'Username': { 'S': 'alexdebrie' },
},
'ConditionExpression': 'attribute_not_exists(PK)'
}
},
{
'Update': {
'TableName': 'UsersTable',
'Key': {
'PK': { 'S': 'POST#1caa5be06389' },
'SK': { 'S': 'POST#1caa5be06389' }
},
'UpdateExpression': 'SET UpvotesCount = UpvotesCount + :incr',
'ExpressionAttributeValues': {
':incr': { 'N': '1' }
}
}
}
]
)

Nossa transa√ß√£o inclui duas solicita√ß√Ķes de grava√ß√£o. O primeiro insere um novo item que indica que o usu√°rio alexdebrie votou positivamente na publica√ß√£o do Reddit. Essa solicita√ß√£o de grava√ß√£o inclui uma express√£o de condi√ß√£o que afirma que um item com a mesma chave ainda n√£o existe, o que indicaria que alexdebrie j√° fez um voto positivo nesse item.

A segunda solicitação de gravação é uma expressão de atualização para incrementar a postagem UpvotesCount votada.

Se a primeira solicitação de gravação falhar porque alexdebrie já fez o voto positivo para esse item, a atualização UpvotesCount não será atualizada, pois toda a transação falhará.

Nota: voc√™ pode manipular esse padr√£o de uma maneira diferente, sem transa√ß√Ķes. Voc√™ pode fazer uma solicita√ß√£o PutItem simples para adicionar um item, indicando que um usu√°rio votou positivamente em um item espec√≠fico, com a mesma express√£o de condi√ß√£o usada acima. Em seguida, usando o DynamoDB Streams, voc√™ pode agregar todas essas novas inser√ß√Ķes de itens e incrementar os itens-pai UpvotesCount em lote.

Essas abordagens s√£o bastante semelhantes, ent√£o voc√™ pode ir de qualquer maneira. Eu recomendaria apenas a abordagem baseada em streams, se uma das seguintes circunst√Ęncias for verdadeira:

  1. Você deseja que o voto positivo seja o mais rápido possível, então você usa a solicitação PutItem que é mais rápida do que a solicitação mais lenta TransactWriteItems.
  2. Voc√™ tem um pequeno n√ļmero de itens que ser√£o votados de modo que seja prov√°vel que voc√™ possa agregar v√°rios upvotes individuais ao incrementar o UpvotesCount.

Vamos pensar um pouco no segundo caso, usando dois exemplos diferentes - um aplicativo de vota√ß√£o nacional e um site de m√≠dia social como o Twitter. No aplicativo de vota√ß√£o, existem apenas algumas op√ß√Ķes para os usu√°rios selecionarem. Voc√™ pode economizar na capacidade de grava√ß√£o em sua tabela usando o m√©todo baseado em streams para agregar v√°rios votos e incrementar o UpvotesCount em lotes.

Por outro lado, um site de mídia social como o Twitter tem uma enorme cardinalidade de itens que podem ser votados. Mesmo com um tamanho de lote de 1000 registros do seu DynamoDB stream, é improvável que você obtenha vários registros que tocam no mesmo item pai. Nesse caso, o uso da capacidade de gravação será o mesmo, pois você precisará incrementar cada item pai individualmente.

Controle de acesso

Um terceiro caso de uso para transa√ß√Ķes est√° em uma configura√ß√£o de autoriza√ß√£o ou controle de acesso.

Imagine que voc√™ forne√ßa um aplicativo SaaS para grandes organiza√ß√Ķes. Com a instala√ß√£o de seu aplicativo por uma organiza√ß√£o, existem dois n√≠veis de usu√°rios: Administradores e Membros. Os administradores t√™m permiss√£o para adicionar novos membros √† instala√ß√£o, mas os membros regulares n√£o.

Sem transa√ß√Ķes, voc√™ precisaria executar um processo de v√°rias etapas para ler e gravar no DynamoDB para adicionar um novo membro. Com as transa√ß√Ķes do DynamoDB, voc√™ pode fazer isso em uma √ļnica etapa.

Imagine que você tem a seguinte tabela:

Esta tabela inclui organiza√ß√Ķes e usu√°rios. Uma organiza√ß√£o tem um padr√£o de chave prim√°ria usado ORG#<OrgName> para PK e SK, enquanto um usu√°rio tem um padr√£o de chave prim√°ria usado ORG#<OrgName> para PK e USER#<Username> SK.

Observe que os itens da organização incluem um atributo Admins do tipo array de strings. Este atributo armazena quais usuários são administradores e, portanto, pode criar novos usuários.

Quando alguém tenta criar um novo usuário em uma organização, você pode usar uma transação para afirmar com segurança que o membro solicitante é um administrador.

A solicitação TransactWriteItems teria a seguinte aparência:

python
response = client.transact_write_items(
TransactItems=[
{
'ConditionCheck': {
'TableName': 'AccessControl',
'Key': {
'PK': { 'S': 'ORG#amazon' },
'SK': { 'S': 'ORG#amazon' },
},
'ConditionExpression': 'contains(Admins, :user)',
'ExpressionAttributeValues': {
':user': { 'S': 'Charlie Bell' }
}
}
},
{
'PutItem': {
'TableName': 'AccessControl',
'Item': {
'PK': { 'S': 'ORG#amazon' },
'SK': { 'S': 'USER#jeffbarr' },
'Username': { 'S': 'jeffbarr' }
}
}
}
]
)

Este exemplo é um pouco diferente do anterior. Observe que estamos usando uma operação ConditionCheck. Na verdade, não queremos modificar o item da organização existente. Nós apenas queremos afirmar uma condição específica: o Charlie Bell é um administrador. Se isso não estiver correto, queremos falhar em toda a transação.

Existem v√°rias maneiras de lidar com a autoriza√ß√£o no seu aplicativo, e essa √© uma op√ß√£o. O bom disso √© que √© flex√≠vel - voc√™ pode implementar a autoriza√ß√£o em todo o aplicativo usando um item singleton em sua tabela ou pode implementar uma autoriza√ß√£o refinada e baseada em recursos, adicionando informa√ß√Ķes de autoriza√ß√£o em um grande n√ļmero de recursos em sua tabela.

Conclus√£o

Nesta postagem, aprendemos sobre as transa√ß√Ķes do DynamoDB. Primeiro, abordamos o b√°sico das transa√ß√Ķes do DynamoDB - como elas funcionam? Que garantias elas fornecem? Quanto elas custam?

Segundo, vimos alguns exemplos de transa√ß√Ķes do DynamoDB em a√ß√£o. Passamos por tr√™s exemplos: mantendo a exclusividade em v√°rios atributos; manipula√ß√£o de contagens e preven√ß√£o de duplicatas; e gerenciamento de controle de acesso.

As transa√ß√Ķes do DynamoDB s√£o uma √≥tima ferramenta para adicionar ao seu cinto de ferramentas. Mas voc√™ tamb√©m precisa estar ciente do impacto no desempenho. Em um pr√≥ximo artigo, testaremos o impacto no desempenho das transa√ß√Ķes em compara√ß√£o com outras alternativas.

Deseja mais conte√ļdo nas transa√ß√Ķes do DynamoDB? Confira o seguinte:

Se você tiver perguntas ou comentários sobre este artigo, sinta-se à vontade para deixar um comentário abaixo ou me envie um email diretamente.

Créditos

Top comments (0)

Join us at DEV
Yes, this is technically an ‚Äúad‚ÄĚ, but really we just want to ask if you want to join DEV. We have 900k+ developers reading, posting, and enjoying community, and would love to have you. ¬† Create an account and continue your coding journey.