Dado o serviço A (CMS) que controla um modelo (Produto, vamos assumir os únicos campos que ele possui são id, título, preço) e serviços B (Remessa) e C (E-mails) que precisam exibir um determinado modelo, qual deve ser a abordagem sincronizar as informações de modelo fornecidas nesses serviços na abordagem de fornecimento de eventos? Vamos supor que o catálogo de produtos raramente mude (mas mude) e que haja administradores que possam acessar dados de remessas e e-mails com muita frequência (as funcionalidades de exemplo são: B: display titles of products the order contained
e C display content of email about shipping that is going to be sent
:). Cada um dos serviços possui seu próprio banco de dados.
Solução 1
Envie todas as informações necessárias sobre o produto no evento - isso significa a seguinte estrutura para order_placed
:
{
order_id: [guid],
product: {
id: [guid],
title: 'Foo',
price: 1000
}
}
No serviço B e C, as informações do produto são armazenadas no product
atributo JSON na orders
tabela
Como tal, para exibir as informações necessárias, apenas os dados recuperados do evento são usados
Problemas : dependendo de quais outras informações precisam ser apresentadas em B e C, a quantidade de dados no evento pode aumentar. B e C podem não exigir as mesmas informações sobre o Produto, mas o evento precisará conter os dois (a menos que os separemos em dois). Se dados não estiverem presentes em determinado evento, o código não poderá ser usado - se adicionarmos uma opção de cor a determinado Produto, para pedidos existentes em B e C, o produto será incolor, a menos que atualizemos os eventos e os executemos novamente .
Solução 2
Enviar apenas guia do produto no evento - isso significa a seguinte estrutura para order_placed
:
{
order_id: [guid],
product_id: [guid]
}
Nos serviços B e C, as informações do produto são armazenadas no product_id
atributo na orders
tabela
As informações do produto são recuperadas pelos serviços B e C quando necessário, executando uma chamada de API para o A/product/[guid]
terminal
Problemas : isso torna B e C dependentes de A (o tempo todo). Se o esquema do Produto mudar em A, é necessário fazer alterações em todos os serviços que dependem deles (de repente)
Solução 3
Enviar apenas guia do produto no evento - isso significa a seguinte estrutura para order_placed:
{
order_id: [guid],
product_id: [guid]
}
Nos serviços B e C, as informações do produto são armazenadas na products
tabela; ainda existe product_id
na orders
tabela, mas há replicação de products
dados entre A, B e C; B e C podem conter informações diferentes sobre o Produto e A
As informações do produto são propagadas quando os serviços B e C são criados e atualizados sempre que as informações sobre os Produtos são alteradas, fazendo uma chamada para o A/product
terminal (que exibe as informações necessárias de todos os produtos) ou executando um acesso direto ao DB de A e copiando as informações necessárias do produto necessárias serviço.
Problemas : isso torna B e C dependentes de A (ao semear). Se o esquema do Produto mudar em A, é necessário fazer alterações em todos os serviços que dependem deles (quando propagação)
Pelo meu entendimento, a abordagem correta seria seguir a solução 1 e atualizar o histórico de eventos por determinada lógica (se o catálogo de produtos não mudou e queremos adicionar cores a serem exibidas, podemos atualizar com segurança o histórico para obter o estado atual de Produtos e preencha os dados ausentes nos eventos) ou atenda à inexistência de dados fornecidos (se o catálogo de produtos mudou e queremos adicionar cores a serem exibidas, não podemos ter certeza se naquele momento no passado, dado o produto tinha uma cor ou não - podemos supor que todos os produtos no catálogo anterior eram pretos e atendidos pela atualização de eventos ou código)
fonte
updating event history
- No caso, o histórico de eventos é a sua fonte de verdade e nunca deve ser alterada, mas apenas seguir em frente. Se os eventos mudarem, você poderá usar a versão do evento ou soluções semelhantes, mas ao reproduzir seus eventos até um momento específico, o estado dos dados deve estar como estava naquele momento.updating event history
Não, quero dizer: passar por todos os eventos, copiando-os de um fluxo (v1) para outro fluxo (v2) para manter um esquema de eventos consistente.display image at the point when purchase was made
) ou não (representar a intenção dedisplay current image as it within catalog
)Respostas:
A solução 3 está realmente próxima da idéia certa.
Uma maneira de pensar sobre isso: B e C estão cada um em cache cópias "locais" dos dados de que precisam. As mensagens processadas em B (e igualmente em C) usam as informações armazenadas em cache localmente. Da mesma forma, os relatórios são produzidos usando as informações armazenadas em cache localmente.
Os dados são replicados da origem para os caches por meio de uma API estável. B e C nem precisam usar a mesma API - eles usam o protocolo de busca apropriado para suas necessidades. Com efeito, definimos um contrato - protocolo e esquema de mensagens - que restringe o provedor e o consumidor. Qualquer consumidor desse contrato pode ser conectado a qualquer fornecedor. Alterações incompatíveis com versões anteriores exigem um novo contrato.
Os serviços escolhem a estratégia de invalidação de cache apropriada para suas necessidades. Isso pode significar extrair alterações da fonte em uma programação regular ou em resposta a uma notificação de que as coisas podem ter mudado, ou até "sob demanda" - atuando como um cache de leitura, voltando à cópia armazenada dos dados quando a fonte não está disponível.
Isso fornece "autonomia", no sentido de que B e C podem continuar a fornecer valor comercial quando A estiver temporariamente indisponível.
Leitura recomendada: Dados externos, Dados internos , Pat Helland 2005.
fonte
Há duas coisas difíceis na Ciência da Computação, e uma delas é a invalidação de cache.
A solução 2 é absolutamente minha posição padrão e, geralmente, você só deve considerar a implementação do cache se encontrar um dos seguintes cenários:
Os problemas de desempenho são realmente o principal driver. Existem muitas maneiras de solucionar o problema nº 2 que não envolvem armazenamento em cache, como garantir que o Serviço A esteja altamente disponível.
O armazenamento em cache adiciona complexidade significativa ao sistema e pode criar casos extremos difíceis de raciocinar e erros difíceis de replicar. Você também precisa reduzir o risco de fornecer dados obsoletos quando existirem dados mais novos, o que pode ser muito pior do ponto de vista comercial do que (por exemplo) exibir uma mensagem de que "O serviço A está inoperante - tente novamente mais tarde".
Deste excelente artigo de Udi Dahan:
Além disso, se você precisar fazer uma consulta pontual dos dados do produto, isso deve ser tratado da maneira como os dados são armazenados no banco de dados do Produto (por exemplo, datas de início / término), deve ser claramente exposto na API (a data efetiva precisa ser uma entrada para a chamada da API para consultar os dados).
fonte
É muito difícil simplesmente dizer que uma solução é melhor que a outra. A escolha de uma entre as Soluções 2 e 3 depende de outros fatores (duração do cache, tolerância de consistência, ...)
Meus 2 centavos:
A invalidação do cache pode ser difícil, mas a declaração do problema menciona que o catálogo de produtos é alterado raramente. Esse fato torna os dados do produto um bom candidato para o cache
Solução 1 (NOK)
Solução 2 (OK)
Solução # 3 (complexa, mas preferida)
fonte
De um modo geral, recomendo fortemente a opção 2 por causa do acoplamento temporal entre esses dois serviços (a menos que a comunicação entre esses serviços seja super estável e não muito frequente). O acoplamento temporal é o que você descreve como
this makes B and C dependant upon A (at all times)
e significa que, se A estiver inativo ou inacessível de B ou C, B e C não podem cumprir sua função.Pessoalmente, acredito que as opções 1 e 3 têm situações em que são opções válidas.
Se a comunicação entre A e B e C for muito alta ou a quantidade de dados necessária para entrar no evento for grande o suficiente para torná-lo uma preocupação, a opção 3 é a melhor opção, porque a carga na rede é muito menor , e a latência das operações diminuirá à medida que o tamanho da mensagem diminuir. Outras preocupações a serem consideradas aqui são:
A opção 1 não é algo que eu descartaria. Existe a mesma quantidade de acoplamento, mas em termos de desenvolvimento deve ser fácil (sem necessidade de ações especiais), e a estabilidade do domínio deve significar que elas não mudam frequentemente (como já mencionei).
Outra opção que eu sugiro é uma pequena variação para 3, que não é executar o processo durante a inicialização, mas sim observar o evento "ProductAdded e" ProductDetailsChanged "em B e C, sempre que houver uma alteração no catálogo de produtos. em A. Isso tornaria suas implantações mais rápidas (e, portanto, mais fáceis de corrigir um problema / bug, se houver).
Editar 2020-03-03
Eu tenho uma ordem específica de prioridades ao determinar a abordagem de integração:
Se o custo da inconsistência for alto (basicamente os dados do produto em A precisam ser consistentes o mais rápido possível com o produto armazenado em cache em B e C), você não poderá evitar a necessidade de aceitar a indisponibilidade e fazer uma solicitação síncrona (como uma Web / rest request) de B & C para A para buscar os dados. Estar ciente! Isso ainda não significa consistente em termos de transação, mas apenas minimiza as janelas de inconsistência. Se você absolutamente, positivamente, precisar ser imediatamente consistente, precisará refazer seus limites de serviço. No entanto, I muito acreditamos fortemente que este não deve ser um problema. Por experiência, é realmente extremamente raro que a empresa não possa aceitar alguns segundos de inconsistência; portanto, você nem precisa fazer solicitações síncronas.
Se você precisar de consultas point-in-time (que eu não notei na sua pergunta e, portanto, não incluímos acima, talvez de forma errada), o custo de manter isso nos serviços downstream é tão alto (você precisaria duplicar lógica interna de projeção de eventos em todos os serviços downstream) que torna a decisão clara: você deve deixar a propriedade para A e consultar uma solicitação ad-hoc pela Web (ou similar) e A deve usar a fonte de eventos para recuperar todos os eventos que você conhecia no momento de projetar para o estado e devolvê-lo. Acho que essa pode ser a opção 2 (se entendi corretamente?), Mas os custos são tais que, enquanto o acoplamento temporal é melhor que o custo de manutenção de eventos duplicados e lógica de projeção.
Se você não precisar de um ponto no tempo e não houver um proprietário claro e único dos dados (que na minha resposta inicial assumi isso com base na sua pergunta), um padrão bastante razoável seria manter representações do produto em cada serviço separadamente. Ao atualizar os dados dos produtos, você atualiza A, B e C em paralelo, fazendo solicitações da Web paralelas para cada uma delas, ou possui uma API de comando que envia vários comandos para cada uma das categorias A, B e C. versão local dos dados para realizar seu trabalho, que pode ou não ser obsoleto. Essa não é uma das opções acima (embora possa ser feita para se aproximar da opção 3), pois os dados em A, B e C podem diferir e o "todo" do produto pode ser uma composição dos três dados fontes.
Saber se a fonte da verdade tem um contrato estável é útil porque você pode usá-lo para usar os eventos de domínio / interno (ou eventos que você armazena na fonte de eventos como padrão de armazenamento em A) para integração entre A e os serviços B e C. Se o contrato for estável, você poderá integrar os eventos do domínio. No entanto, você tem uma preocupação adicional no caso em que as alterações são frequentes ou esse contrato de mensagem é grande o suficiente para tornar o transporte uma preocupação.
Se você tiver um proprietário claro, com um contrato que se espera estável, as melhores opções seriam a opção 1; um pedido conteria todas as informações necessárias e, em seguida, B e C executariam sua função usando os dados no evento.
Se o contrato é suscetível de alterar ou quebrar com frequência, seguindo a opção 3, que é voltar às solicitações da Web para buscar dados do produto, na verdade é uma opção melhor, pois é muito mais fácil manter várias versões. Então, B faria uma solicitação na v3 do produto.
fonte
ProductAdded
ouProductDetailsChanged
adicione complexidade ao rastreamento de alterações no catálogo de produtos, precisamos manter esses dados sincronizados entre os bancos de dados de alguma forma, caso os eventos sejam reproduzidos e precisamos acessar os dados do catálogo do passado.