Digamos que tenhamos um microsserviço de Usuário, Carteira REST e um gateway de API que cole as coisas. Quando Bob se registra em nosso site, nosso gateway de API precisa criar um usuário através do microsserviço de usuário e uma carteira através do microsserviço de carteira.
Agora, aqui estão alguns cenários em que as coisas podem dar errado:
A criação do usuário Bob falha: tudo bem, apenas retornamos uma mensagem de erro ao Bob. Estamos usando transações SQL para que ninguém nunca tenha visto Bob no sistema. Tudo otimo :)
O usuário Bob é criado, mas, antes que nossa Carteira virtual possa ser criada, nosso gateway de API falha muito. Agora temos um usuário sem carteira (dados inconsistentes).
O usuário Bob é criado e, ao criar a Carteira virtual, a conexão HTTP cai. A criação da carteira pode ter sido bem-sucedida ou não.
Quais soluções estão disponíveis para impedir que esse tipo de inconsistência de dados ocorra? Existem padrões que permitem que as transações abranjam várias solicitações REST? Eu li a página da Wikipedia no commit de duas fases que parece abordar esse problema, mas não sei como aplicá-lo na prática. Este Transações Distribuídas Atômicas: um documento de design RESTful também parece interessante, embora eu ainda não o tenha lido.
Como alternativa, eu sei que o REST pode não ser adequado para este caso de uso. Talvez a maneira correta de lidar com essa situação abandone totalmente o REST e use um protocolo de comunicação diferente como um sistema de fila de mensagens? Ou devo impor consistência no código do meu aplicativo (por exemplo, tendo um trabalho em segundo plano que detecta inconsistências e as corrige ou tendo um atributo "state" no meu modelo de usuário com valores "criando", "criado" etc.)?
fonte
Respostas:
O que não faz sentido:
O que lhe dará dores de cabeça:
Qual é provavelmente a melhor alternativa:
Mas e se você precisar de respostas síncronas?
fonte
Essa é uma pergunta clássica que me foi feita durante uma entrevista recentemente Como chamar vários serviços da Web e ainda preservar algum tipo de tratamento de erro no meio da tarefa. Hoje, na computação de alto desempenho, evitamos confirmações em duas fases. Eu li um artigo há muitos anos sobre o que foi chamado de "modelo Starbuck" para transações: pense no processo de pedido, pagamento, preparação e recebimento do café que você pede na Starbuck ... Eu simplifico demais as coisas, mas um modelo de confirmação em duas fases seria sugira que todo o processo seja uma transação única para todas as etapas envolvidas até você receber seu café. No entanto, com esse modelo, todos os funcionários esperariam e parariam de trabalhar até você tomar seu café. Você vê a foto?
Em vez disso, o "modelo Starbuck" é mais produtivo, seguindo o modelo de "melhor esforço" e compensando os erros no processo. Primeiro, eles garantem que você pague! Depois, há filas de mensagens com seu pedido anexado ao copo. Se algo der errado no processo, como você não recebeu seu café, não foi o que pediu, etc., entramos no processo de compensação e garantimos que você obtém o que deseja ou o devolve. Este é o modelo mais eficiente para aumentar a produtividade.
Às vezes, a starbuck está desperdiçando um café, mas o processo geral é eficiente. Existem outros truques para pensar quando você cria seus serviços da Web, como projetá-los de uma maneira que eles podem ser chamados inúmeras vezes e ainda assim fornecem o mesmo resultado final. Então, minha recomendação é:
Não fique muito bem ao definir seus serviços da Web (não estou convencido sobre o hype de microsserviços que acontece hoje em dia: muitos riscos de ir longe demais);
Async aumenta o desempenho; portanto, prefira ser assíncrono, envie notificações por email sempre que possível.
Crie serviços mais inteligentes para torná-los "recuperáveis" várias vezes, processando com um uid ou taskid que seguirá o pedido até o final, validando as regras de negócios em cada etapa;
Use filas de mensagens (JMS ou outras) e desvie para processadores de tratamento de erros que aplicarão operações à "reversão" aplicando operações opostas. A propósito, trabalhar com ordem assíncrona exigirá algum tipo de fila para validar o estado atual do processo, então considere isso;
Por último, (como isso pode não acontecer com frequência), coloque-o em uma fila para processamento manual de erros.
Vamos voltar com o problema inicial que foi postado. Crie uma conta e crie uma carteira e verifique se tudo foi feito.
Digamos que um serviço da Web seja chamado para orquestrar toda a operação.
O pseudo-código do serviço da web ficaria assim:
Ligue para o microsserviço de criação de conta, passe algumas informações e um ID de tarefa exclusivo 1.1 O microsserviço de criação de conta verificará primeiro se essa conta já foi criada. Um ID de tarefa está associado ao registro da conta. O microsserviço detecta que a conta não existe e, portanto, a cria e armazena o ID da tarefa. NOTA: este serviço pode ser chamado 2000 vezes, sempre executará o mesmo resultado. O serviço responde com um "recibo que contém informações mínimas para executar uma operação de desfazer, se necessário".
Ligue para a criação do Wallet, fornecendo o ID da conta e o ID da tarefa. Digamos que uma condição não seja válida e a criação da carteira não possa ser executada. A chamada retorna com um erro, mas nada foi criado.
O orquestrador é informado do erro. Ele sabe que precisa interromper a criação da conta, mas não fará isso sozinho. Ele solicitará que o serviço de carteira faça isso passando seu "recibo mínimo de desfazer" recebido no final da etapa 1.
O serviço de conta lê o recebimento de desfazer e sabe como desfazer a operação; o recebimento de desfazer pode até incluir informações sobre outro microsserviço que ele poderia ter chamado para fazer parte do trabalho. Nessa situação, o recebimento de desfazer pode conter o ID da conta e possivelmente algumas informações adicionais necessárias para executar a operação oposta. No nosso caso, para simplificar, digamos que é simplesmente excluir a conta usando seu ID.
Agora, digamos que o serviço da Web nunca tenha recebido o sucesso ou a falha (neste caso) de que o desfazer da criação da conta foi realizado. Ele simplesmente chamará o serviço de desfazer da conta novamente. E esse serviço normalmente nunca falha, porque seu objetivo é que a conta não exista mais. Por isso, verifica se existe e vê que nada pode ser feito para desfazê-lo. Portanto, retorna que a operação é um sucesso.
O serviço da web retorna ao usuário que a conta não pôde ser criada.
Este é um exemplo síncrono. Poderíamos ter gerenciado de maneira diferente e colocado o caso em uma fila de mensagens direcionada ao suporte técnico, se não quisermos que o sistema recupere completamente o erro ". Já vi isso sendo realizado em uma empresa onde não é suficiente ganchos podem ser fornecidos ao sistema de back-end para corrigir situações.O suporte técnico recebeu mensagens contendo o que foi executado com êxito e tinha informações suficientes para corrigir coisas, assim como nosso recibo de desfazer poderia ser usado de uma maneira totalmente automatizada.
Eu realizei uma pesquisa e o site da microsoft tem uma descrição padrão para essa abordagem. É chamado de padrão de transação compensadora:
Padrão de transação de compensação
fonte
Todos os sistemas distribuídos têm problemas com a consistência transacional. A melhor maneira de fazer isso é como você disse, com um commit em duas fases. Faça com que a carteira e o usuário sejam criados em um estado pendente. Após a criação, faça uma chamada separada para ativar o usuário.
Esta última chamada deve ser repetida com segurança (caso sua conexão caia).
Isso exigirá que a última chamada conheça as duas tabelas (para que isso possa ser feito em uma única transação JDBC).
Como alternativa, você pode pensar sobre o motivo de sua preocupação com um usuário sem carteira. Você acredita que isso causará um problema? Nesse caso, talvez seja uma má idéia tê-las como chamadas de descanso separadas. Se um usuário não existir sem uma carteira, provavelmente você deverá adicionar a carteira ao usuário (na chamada POST original para criar o usuário).
fonte
IMHO, um dos principais aspectos da arquitetura de microsserviços é que a transação está confinada ao microsserviço individual (princípio de responsabilidade única).
No exemplo atual, a criação do usuário seria uma transação própria. A criação do usuário enviaria um evento USER_CREATED para uma fila de eventos. O serviço da Carteira assinaria o evento USER_CREATED e faria a criação da Carteira.
fonte
Se minha carteira fosse apenas mais um monte de registros no mesmo banco de dados sql que o usuário, provavelmente colocaria o código de criação de usuário e carteira no mesmo serviço e lidaria com isso usando os recursos normais de transação do banco de dados.
Parece-me que você está perguntando sobre o que acontece quando o código de criação de carteira exige que você toque em outro sistema ou sistemas? Eu diria que tudo depende de quão complexo e ou arriscado é o processo de criação.
Se for apenas uma questão de tocar em outro armazenamento de dados confiável (por exemplo, um que não possa participar de suas transações sql), dependendo dos parâmetros gerais do sistema, eu posso estar disposto a arriscar a chance muito pequena de que a segunda gravação não ocorra. Talvez eu não faça nada, mas crie uma exceção e lide com os dados inconsistentes por meio de uma transação compensadora ou mesmo algum método ad-hoc. Como sempre digo aos meus desenvolvedores: "se esse tipo de coisa está acontecendo no aplicativo, não passa despercebido".
À medida que a complexidade e o risco da criação de carteira aumentam, você deve tomar medidas para melhorar os riscos envolvidos. Digamos que algumas das etapas exijam a chamada de várias APIs de parceiros.
Nesse ponto, você pode introduzir uma fila de mensagens junto com a noção de usuários e / ou carteiras parcialmente construídos.
Uma estratégia simples e eficaz para garantir que suas entidades acabem sendo construídas adequadamente é que os trabalhos sejam repetidos até que sejam bem-sucedidos, mas depende muito dos casos de uso do seu aplicativo.
Eu também pensava muito sobre por que tive uma etapa propensa a falhas no meu processo de provisionamento.
fonte
Uma solução simples é criar usuário usando o Serviço do Usuário e usar um barramento de mensagens em que o serviço do usuário emite seus eventos, e o Serviço Wallet se registra no barramento de mensagens, ouve o evento Criado pelo Usuário e cria a Carteira para o Usuário. Enquanto isso, se o usuário acessar a interface do usuário da Carteira virtual do Google Wallet, verifique se o usuário acabou de ser criado e mostre que a criação da sua carteira está em andamento, verifique-a em algum momento
fonte
Tradicionalmente, os gerenciadores de transações distribuídas são usados. Alguns anos atrás, no mundo Java EE, você pode ter criado esses serviços como EJBs, que foram implementados em diferentes nós e seu gateway de API teria feito chamadas remotas para esses EJBs. O servidor de aplicativos (se configurado corretamente) garante automaticamente, usando a confirmação em duas fases, que a transação seja confirmada ou revertida em cada nó, para garantir a consistência. Mas isso exige que todos os serviços sejam implantados no mesmo tipo de servidor de aplicativos (para que sejam compatíveis) e, na realidade, apenas trabalhem com serviços implantados por uma única empresa.
Para SOAP (ok, não REST), existe a especificação WS-AT, mas nenhum serviço que eu já tive que integrar suporta isso. Para o REST, o JBoss tem algo em andamento . Caso contrário, o "padrão" é encontrar um produto que você possa conectar à sua arquitetura ou criar sua própria solução (não recomendado).
Publiquei esse produto para Java EE: https://github.com/maxant/genericconnector
De acordo com o documento que você mencionou, também há o padrão Try-Cancel / Confirm e o Produto associado da Atomikos.
Os mecanismos BPEL lidam com a consistência entre serviços implantados remotamente usando compensação.
Existem várias maneiras de "vincular" recursos não transacionais a uma transação:
O papel dos diabos defendem: por que construir algo assim, quando existem produtos que fazem isso por você (veja acima) e provavelmente o fazem melhor do que você pode, porque eles são experimentados e testados?
fonte
Pessoalmente, gosto da ideia de Micro Services, módulos definidos pelos casos de uso, mas, como sua pergunta menciona, eles têm problemas de adaptação para empresas clássicas como bancos, seguros, telecomunicações, etc ...
As transações distribuídas, como muitos mencionadas, não são uma boa escolha, as pessoas agora buscam mais sistemas eventualmente consistentes, mas não tenho certeza se isso funcionará para bancos, seguros, etc.
Eu escrevi um blog sobre a minha solução proposta, pode ser que isso possa ajudá-lo ....
https://mehmetsalgar.wordpress.com/2016/11/05/micro-services-fan-out-transaction-problems-and-solutions-with-spring-bootjboss-and-netflix-eureka/
fonte
A consistência eventual é a chave aqui.
O comandante é responsável pela transação distribuída e assume o controle. Ele conhece a instrução a ser executada e coordenará a execução. Na maioria dos cenários, haverá apenas duas instruções, mas ele pode lidar com várias instruções.
O comandante assume a responsabilidade de garantir a execução de todas as instruções, e isso significa se aposentar. Quando o comandante tenta efetuar a atualização remota e não obtém uma resposta, não é possível tentar novamente. Dessa forma, o sistema pode ser configurado para ser menos propenso a falhas e se recupera.
Como temos novas tentativas, temos idempotência. Idempotência é a propriedade de poder fazer algo duas vezes, de forma que os resultados finais sejam os mesmos, como se tivessem sido feitos apenas uma vez. Precisamos de idempotência no serviço remoto ou na fonte de dados para que, no caso em que receba a instrução mais de uma vez, a processe apenas uma vez.
Consistência eventual Isso resolve a maioria dos desafios de transações distribuídas, no entanto, precisamos considerar alguns pontos aqui. Toda transação com falha será seguida de uma nova tentativa; a quantidade de tentativas repetidas depende do contexto.
A consistência é eventual, ou seja, enquanto o sistema está fora do estado consistente durante uma nova tentativa, por exemplo, se um cliente encomendou um livro, efetuou um pagamento e atualiza a quantidade em estoque. Se as operações de atualização de estoque falharem e supondo que esse foi o último estoque disponível, o livro continuará disponível até que a operação de repetição da atualização de estoque tenha sido bem-sucedida. Depois que a nova tentativa for bem-sucedida, seu sistema será consistente.
fonte
Por que não usar a plataforma API Management (APIM) que suporta scripts / programação? Portanto, você poderá criar um serviço composto no APIM sem perturbar os microsserviços. Eu projetei usando o APIGEE para essa finalidade.
fonte