DEV Community

Cover image for Consistência de dados e padrão Outbox
Rafael
Rafael

Posted on • Edited on

Consistência de dados e padrão Outbox

Um dos pontos mais importantes em todas aplicações que trabalha com escrita de dados é garantir a consistência dos dados.

Um exemplo básico é a criação de um pedido que possui itens. Considerando um banco relacional, os dados são armazenados em duas tabelas Orders e OrderItems. Imagine que a aplicação realize um INSERT no banco na tabela Orders e recebe um erro na tentativa de fazer o INSERT na tabela OrderItems. Dessa forma o sistema ficou inconsistente, o usuário tem um pedido sem itens e isso pode, inclusive, impedir que ele consiga repetir o pedido.

Aplicação de Pedidos que armazena no banco de dados

O desejado é que o pedido seja salvo completamente ou que seja nada salvo. Nos bancos relacionais a solução é o uso de transações.

BEGIN TRANSACTION
  INSERT INTO TABLE Orders...
  INSERT INTO TABLE OrderItems...
COMMIT
Enter fullscreen mode Exit fullscreen mode

Caso o segundo INSERT não seja concluído deve ser realizado o rollback da transação mantendo o comportamento de "tudo ou nada".

O problema de escrita dupla (dual write)

Agora vamos ampliar nosso cenário, um microsserviço de Pedidos é responsável por manter os dados de pedidos e notificar os outros sistemas sobre pedidos feitos. Para se comunicar com os outros serviços será utilizado uma solução de mensageria.

A aplicação salva pedidos no banco de dados e envia uma mensagem para um sistema de mensageria

Dessa forma, ao receber um novo pedido deve ser feita uma escrita na tabela Orders no banco de dados e enviar uma mensagem order.created para uma fila. Como manter a consistência entre o banco de dados e o sistema de mensageria? Se, após inserir um registro no banco de dados, o envio da mensagem para a fila não tiver sucesso as aplicações que esperam a mensagem ficam inconsistentes. Invertendo a lógica e enviando a mensagem antes de salvar no banco de dados produz um problema maior pois as aplicações seguintes podem receber mensagens de operações que nunca foram registradas na aplicação que é dona desse dado.

Manter a consistência dos dados em sistemas distribuídos é um grande desafio e diversas abordagens tem diferentes trade-offs. Comumente, é aceitável garantir que o sistema esteja consistente em algum ponto no futuro. Essa abordagem é chamada de Consistência Posterior (eventual consistency).

Outbox Pattern

A ideia do outbox pattern é usar o banco de dados para garantir que a mensagem será enviada. As mensagens que serão enviadas para o sistema de mensageria devem ser armazenadas em uma tabela junto do status de envio. Ao inserir um registro da tabela Orders também deve-se inserir a mensagem, tudo dentro de uma transação.

BEGIN TRANSACTION
  INSERT INTO Orders...
  INSERT INTO OutboxMessages(message, status)...
COMMIT
Enter fullscreen mode Exit fullscreen mode

Essa tabela OutboxMessages deve ser verificada periodicamente em busca de mensagens com o status pendente para que sejam enviadas para a fila. Dessa forma tem-se a garantia que a mensagem será enviada ao menos uma vez. Por que ao menos uma vez? Porque o processo de envio da mensagem e a atualização do status de envio do registro do banco é uma escrita dupla e está sujeita a falhas. A operação de atualização do status como enviado pode falhar e portanto durante a próxima busca de mensagens pendentes essa mensagem seria enviada novamente.

Idempotência

Considerando o problema anterior é possível que uma mesma mensagem seja enviada repetidamente para a fila. Assim, é necessário que os sistemas que processam essas mensagens sejam idempotentes, ou seja, a execução de operações repetidas não podem afetar o resultado da primeira operação. Algumas operações são idempotentes por natureza, como atribuir um determinado valor para um campo de um registro de uma tabela. Se executada múltiplas vezes o resultado final é o mesmo de se executar apenas uma vez. Em outras situações não garantir a idempotência pode ser catastrófico. Expandindo nosso exemplo, considere uma aplicação que recebe mensagens de pedidos realizados e decrementa o estoque dos produtos vendidos. Se a mensagem do pedido for duplicada, o pedido abateria do estoque o dobro de produtos comprados.

O serviço de Produtos processa a mensagem de pedidos realizados e atualiza no seu banco de dados

Uma maneira de implementar idempotência para esses casos é utilizar novamente o banco de dados. A aplicação que consome as mensagens deve possuir uma tabela para armazenar o id das mensagens processadas. Ao executar a operação no banco de dados, no nosso exemplo um UPDATE na tabela Products, deve-se inserir o id da mensagem na tabela de mensagens processadas. As duas operações devem ser realizadas dentro de uma transação pois se a restrição de chave primária da tabela ProccessedMessages retornar erro (ou seja, a mensagem já foi processada) a atualização do produto deve ser desfeita.

BEGIN TRANSACTION
  UPDATE Produtcs...
  INSERT INTO ProccessedMessages(Id) ...
COMMIT

Enter fullscreen mode Exit fullscreen mode

Implementação

Usando C# existem algumas opções de bibliotecas que implementam o padrão Outbox como CAP e Mass Transit. Para idempotência eu criei o Ziggurat que implementa a solução descrita nesse texto. É possível ver um exemplo de implementação de CAP com Ziggurat aqui.

No python existe a biblioteca django-outbox-pattern que implementa o padrão outbox e também garante idempotência nos consumidores.

Top comments (0)