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