Veja também nosso curso de extensão a distância, com certificados emitidos pelo DCC/UFMG.
Como discutimos no Capítulo 7, em arquiteturas baseadas em microsserviços, recomenda-se que cada microsserviço possua sua própria base de dados. Ou seja, recomenda-se uma arquitetura como a seguinte.
Por outro lado, ao adotar essa recomendação, frequentemente surge o seguinte problema: como garantir a consistência dos dados do sistema quando eles estão distribuídos em vários microsserviços?
Para ilustrar, vamos usar um exemplo de uma loja virtual. Nessa loja, para concluir uma venda temos que realizar duas operações:
op1
: dar baixa nos itens vendidos (no microsserviço de
estoque)op2
: processar o pagamento (no microsserviço de
pagamento)Essas duas operações devem constituir uma transação, o que vai garantir que elas serão executadas de forma atômica. Mais especificamente, atomicidade significa que apenas dois resultados são possíveis:
Nessa explicação, executar significa ter seus efeitos registrados no banco de dados. Portanto, o que não pode ocorrer é executar uma operação e não executar a outra, pois isso deixaria o sistema em um estado inconsistente.
A seguir, vamos discutir as maneiras tradicionais para garantir atomicidade. Primeiro, em bancos de dados centralizados. Depois, em bancos de dados distribuídos.
Quando um sistema possui uma arquitetura monolítica, normalmente
temos um único banco de dados. Nesses casos, a própria implementação do
banco garante a execução atômica de transações, por meio de comandos
commit
e rollback
.
Podemos então usar um código como o seguinte:
try {
op1();
op2();
commit();
} catch (Failure) {
rollback();
}
Se op1
e op2
terminarem suas execuções com
sucesso, chamamos commit
para persistir os resultados no
banco de dados. Por outro lado, se qualquer uma das operações falhar,
chamamos rollback
para retornar o banco ao seu estado
inicial.
Suponha, no entanto, que op1
executa em um banco de
dados e op2
executa em um outro banco de dados, como
tipicamente ocorre no caso de arquiteturas baseadas em microsserviços.
Nesse novo cenário, não existe mais garantia automática de
atomicidade.
Uma possível solução requer o uso de um protocolo que garanta atomicidade na execução de operações distribuídas. O mais conhecido deles é chamado de Two Phase Commit (2PC). Apenas para ficar um pouco mais claro, esse protocolo normalmente é implementado e disponibilizado pelo próprio banco de dados distribuído.
No entanto, os problemas de 2PC são bem conhecidos. Por exemplo, o protocolo tem um custo e uma latência altos. Isso ocorre porque os processos participantes de uma transação distribuída têm que trocar diversas mensagens, antes de chegarem a um consenso sobre o seu resultado. E, no limite, pode-se chegar a uma situação de impasse. Isto é, a transação pode ficar bloqueada por um tempo indeterminado no caso de queda do processo coordenador do protocolo.
Por isso, alguns autores recomendam explicitamente que microsserviços não devem usar 2PC. Veja, por exemplo, a recomendação de Sam Newman (link):
Eu recomendo fortemente que você evite o uso de transações distribuídas e de 2PC para coordenar mudanças de estado em microsserviços.
Por isso, começou-se a procurar alternativas mais viáveis para consistência dos dados de microsserviços. Uma delas é o conceito de sagas, que descreveremos a seguir.
Sagas é um conceito antigo de bancos de dados, proposto em 1987 por Hector Garcia-Molina e Kenneth Salem. Se quiser, veja o artigo original, que é muito claro e fácil de ler.
Originalmente, o conceito foi proposto para tratar transações de longa duração. No entanto, modernamente, ele está sendo aplicado também no contexto de arquiteturas baseadas em microsserviços.
Uma saga é definida por meio de dois conjuntos:
Um conjunto de transações T1, T2,…, Tn (que devem ser executadas nesta ordem).
Um conjunto de compensações para cada transação, que vamos chamar de C1, C2,…, Cn. Ou seja, toda transação tem uma segunda transação que a reverte. Por exemplo, uma transação de crédito de x reais é compensada por uma transação de débito do mesmo valor.
Idealmente, desejamos que todas as transações Ti sejam executadas com sucesso e sequencialmente, começando em T1 e terminando em Tn. Esse é o caminho feliz de uma saga.
Porém, principalmente em ambientes distribuídos (como é o caso de microsserviços), pode ser que uma transação intermediária Tj falhe, isto é:
T1 (sucesso), T2 (sucesso),…, Tj (falha)
Quando isso ocorre, temos então que executar as devidas compensações para as transações anteriores. Ou seja, continuamos assim:
T1 (sucesso), T2 (sucesso),…, Tj (falha), Cj-1,…, C2, C1.
Estamos assumindo que quando Tj falha ela não registrou seus efeitos no banco de dados. Logo, não precisamos chamar Cj, mas apenas as compensações de Cj-1 até C1.
Para concluir, vamos mostrar como seria o código para implementar uma saga formada por três transações:
try {
T1();
T2();
T3();
}
catch (FailureT1) {
// sem compensação
}
catch (FailureT2) {
C1();
}
catch (FailureT3) {
C2();
C1();
}
1. Por que microsserviços não devem compartilhar um único banco de dados? Para responder, você pode consultar a Seção 7.4.1 do Capítulo 7 e também o início da Seção 7.4.
2. Qual a diferença entre uma transação distribuída e uma saga? Mais especificamente:
Quando consideradas individualmente, as transações de uma saga são atômicas?
Se não houvesse compensações, as transações de uma saga, quando consideradas em conjunto, seriam sempre atômicas?
Suponha uma transação Ti de uma saga. Uma transação T’ que não faz parte da saga pode observar os resultados de Ti antes da execução completa da saga?
Suponha uma transação distribuída T. Uma segunda transação T’ pode observar os resultados ainda intermediários de T?
Com sagas, temos que escrever a lógica de rollback, isto é, o código das compensações. O mesmo acontece com transações distribuídas? Sim ou não? Justifique.
3. Como um desenvolvedor deve proceder quando uma compensação Ci falhar (isto é, não puder ser executada com sucesso)?
4. Qual problema de transações de longa duração é resolvido por meio de sagas? Se necessário, consulte o segundo parágrafo da Introdução do artigo que definiu o conceito.
Voltar para a lista de artigos.