Como garantir a idempotência da interface

Por que precisamos garantir a idempotência da interface

Porque em muitos cenários de negócios, para garantir a correção dos negócios, muitas vezes há solicitações repetidas ou mensagens repetidas. Especialmente em sistemas distribuídos, devido à instabilidade da rede ou balanceamento de carga, podem ocorrer requisições repetidas. Se a interface não for idempotente, fará com que a mesma operação seja repetida várias vezes, resultando em atualizações incorretas de dados ou outros problemas

Exemplo de cenário

  1. Ao preencher alguns formulários, acidentalmente clicamos no botão salvar duas vezes rapidamente e dois dados duplicados foram gerados na tabela, mas os ids eram diferentes.
  2. Para resolver o problema de timeout da interface em nosso projeto, um mecanismo de repetição foi introduzido. Na primeira vez que a interface de solicitação expirou, o solicitante falhou em obter o resultado retornado a tempo (pode ter conseguido neste momento), para evitar retornar o resultado errado (é impossível retornar falha diretamente neste caso?) , então a solicitação é repetida Tente algumas vezes, isso também produzirá dados duplicados.
  3. Quando os consumidores mq lêem mensagens, eles às vezes leem mensagens duplicadas.Se mq não habilitar strict exactamenteOnce, resultando em dados duplicados.

A idempotência da interface significa que os resultados de uma solicitação ou várias solicitações iniciadas pelo usuário para a mesma operação são consistentes e não haverá efeitos colaterais devido a vários cliques.

Esse tipo de problema ocorre principalmente na interface:

Operação de inserção, neste caso, múltiplas requisições podem gerar dados duplicados.
Se a operação de atualização for simplesmente atualizar dados, por exemplo: update user set status=1 where id=1, não há problema. Se ainda houver cálculos, como: update user set status=status+1 where id=1, neste caso, múltiplas requisições podem resultar em erros de dados.

solução

selecionar antes de inserir

Normalmente, na interface de salvar dados, para evitar dados duplicados, geralmente selecionamos dados de acordo com o nome ou campo de código antes de inserir. Se os dados já existirem, a operação de atualização será executada e, se os dados não existirem, a operação de inserção será executada.

Essa solução pode ser a que mais usamos para evitar a geração de dados duplicados. Porém, em cenários concorrentes, deve ser utilizado em conjunto com outras soluções, caso contrário também serão gerados dados duplicados.

Adicionar bloqueio pessimista

No cenário de pagamento, o saldo da conta do usuário A é de 150 yuans e ele deseja transferir 100 yuans.Em circunstâncias normais, o saldo do usuário A é de apenas 50 yuans. Em geral, sql é assim:

atualizar valor do usuário = valor-100 onde id=123;
Se a mesma solicitação ocorrer várias vezes, o saldo do usuário A pode se tornar negativo. Nesse caso, o usuário A pode chorar. Ao mesmo tempo, os desenvolvedores do sistema também podem chorar, porque esse é um bug de sistema muito sério.

Para resolver este problema, bloqueios pessimistas podem ser adicionados para bloquear a linha de dados do usuário A. Ao mesmo tempo, apenas uma solicitação é permitida para obter o bloqueio e atualizar os dados, enquanto outras solicitações aguardam.

Normalmente, uma única linha de dados é bloqueada pelo seguinte SQL:

selecione * de user id=123 para atualização;

O processo específico é o seguinte:

Várias solicitações consultam informações do usuário com base no id ao mesmo tempo.
Determine se o saldo é menor que 100 e, se for insuficiente, retorne o saldo insuficiente diretamente.
Se o saldo for suficiente, consulte novamente as informações do usuário por meio de atualização e tente adquirir o bloqueio.
Somente a primeira requisição pode adquirir o bloqueio de linha, e as demais requisições que não adquirirem o bloqueio aguardam a próxima oportunidade de adquirir o bloqueio.
Após a primeira solicitação adquirir o bloqueio, é julgado se o saldo é menor que 100 e, se o saldo for suficiente, a operação de atualização é executada.
Caso o saldo seja insuficiente, significa que a requisição foi repetida, e retornará diretamente com sucesso.

Atenção especial é necessária: se você estiver usando o banco de dados mysql, o mecanismo de armazenamento deve usar o innodb, pois ele suporta apenas transações. Além disso, o campo id aqui deve ser uma chave primária ou um índice único, caso contrário, toda a tabela será bloqueada.

Adicionar bloqueio otimista

O bloqueio pessimista precisa bloquear uma linha de dados durante a mesma operação de transação.Se a transação demorar muito, fará com que um grande número de solicitações espere e afete o desempenho da interface.

Para melhorar o desempenho da interface, podemos usar o bloqueio otimista. Um carimbo de data/hora ou campo de versão precisa ser adicionado à tabela. Aqui, tomamos o campo de versão como exemplo.

Consulte os dados antes de atualizá-los:

selecione id, valor, versão do usuário id=123;

Se os dados existirem, assuma que a versão encontrada é igual a 1 e, em seguida, use os campos id e version como condições de consulta para atualizar os dados:

atualize o conjunto do usuário valor=valor+100, versão=versão+1 onde id=123 e versão=1;

Ao atualizar os dados, versão+1 e, em seguida, julgue que o número de linhas afetadas pela operação de atualização é maior que 0, indicando que a atualização foi bem-sucedida e, se for igual a 0, indicando que a atualização falhou.

Como a primeira solicitação com versão igual a 1 pode ser bem-sucedida, a versão se torna 2 após a operação ser bem-sucedida. Neste momento, se vierem solicitações simultâneas, execute o seguinte sql:

atualize o conjunto do usuário valor=valor+100, versão=versão+1 onde id=123 e versão=1;

A operação de atualização não atualizará realmente os dados. No final, o número de linhas afetadas pelo resultado da execução do SQL é 0, porque a versão se tornou 2 e a versão = 1 em onde não deve satisfazer a condição. No entanto, para garantir a idempotência da interface, a interface pode retornar diretamente o sucesso, pois o valor da versão foi modificado, portanto o sucesso anterior deve ter sido feito uma vez e as solicitações subsequentes são repetidas.

O processo específico é o seguinte:

Etapas específicas:

Primeiro, consulte as informações do usuário de acordo com o id, incluindo o campo da versão. Use
os valores dos campos id e version como parâmetros da condição where para atualizar as informações do usuário. Ao mesmo tempo, a versão+1
determina o número de linhas afetado pela operação. Se afetar 1 linha, significa que é uma solicitação e você pode fazer outras coisas de manipulação de dados.
Se 0 linhas forem afetadas, significa que a solicitação é repetida e o sucesso será retornado diretamente.

Adicionar um índice exclusivo

Na maioria dos casos, para evitar dados duplicados, adicionaremos um índice exclusivo à tabela.

alter table orderadd UNIQUE KEY un_code( code);

Depois de adicionar um índice exclusivo, os dados da primeira solicitação podem ser inseridos com sucesso. Mas para a mesma solicitação posteriormente, ao inserir dados, uma entrada duplicada '002' para a chave 'exceção order.un_code será relatada, indicando que há um conflito no índice exclusivo. Embora o lançamento de uma exceção não tenha efeito nos dados, isso não causará dados incorretos. Mas para garantir a idempotência da interface, precisamos capturar a exceção e retornar com sucesso.

Se for um programa java, você precisa capturar: exceção DuplicateKeyException, se você usar o framework spring, você também precisa capturar: exceção MySQLIntegrityConstraintViolationException.

Etapas específicas:

O usuário inicia uma solicitação por meio do navegador e o servidor coleta os dados.
Insira os dados no mysql
para determinar se a execução foi bem-sucedida e, se for bem-sucedida, opere outros dados (e possivelmente outra lógica de negócios).
Se a execução falhar, a exceção de conflito de índice exclusivo é capturada e o sucesso é retornado diretamente.

relógio anti-pesado

Às vezes, nem todos os cenários na tabela podem gerar dados duplicados, mas alguns cenários não são permitidos. Neste momento, obviamente não é apropriado adicionar diretamente um índice exclusivo à tabela.

Em resposta a esta situação, podemos resolver o problema construindo uma mesa antipesada.

A tabela só pode conter dois campos: id e índice único.O índice único pode ser um identificador único combinado com vários campos como nome, código, etc., por exemplo: susan_0001.

O fluxograma específico é o seguinte: Etapas específicas:

O usuário inicia uma solicitação por meio do navegador e o servidor coleta os dados.
Insira os dados na tabela antipesada do mysql
para determinar se a execução foi bem-sucedida. Se for bem-sucedida, execute outras operações de dados no mysql (possivelmente outra lógica de negócios).
Se a execução falhar, a exceção de conflito de índice exclusivo é capturada e o sucesso é retornado diretamente.
Atenção especial deve ser dada: a tabela antiduplicação e a tabela de negócios devem estar no mesmo banco de dados e as operações devem estar na mesma transação.

De acordo com a máquina do estado

Em muitos casos, a tabela de negócios tem estados, por exemplo, a tabela de pedidos tem: 1-pedido, 2-pago, 3-concluído, 4-cancelado e outros estados. Se os valores desses estados forem regulares, podemos usá-lo para garantir a idempotência da interface de acordo com o fato de os nós de negócios serem de pequeno a grande porte.

Se o status do pedido de id=123 for pago, ele será concluído.

atualizar orderconjunto status=3 onde id=123 e status=2;

Quando a primeira solicitação é feita, o status do pedido é pago e o valor é 2, então a instrução de atualização pode atualizar os dados normalmente, o número de linhas afetadas pelo resultado da execução do SQL é 1 e o status do pedido se torna 3.

Quando a mesma requisição vem depois, quando o mesmo sql é executado, o status do pedido passa a ser 3, e status=2 é usado como condição, os dados a serem atualizados não podem ser consultados, então o número de linhas afetadas pela execução final do sql o resultado é 0. Ou seja, os dados não serão realmente atualizados. No entanto, para garantir a idempotência da interface, quando o número de linhas afetadas for 0, a interface também pode retornar sucesso diretamente.

O fluxograma específico é o seguinte:

Etapas específicas:

O usuário inicia uma solicitação por meio do navegador e o servidor coleta os dados.
Com base no id e no estado atual como condições, atualize para o próximo estado.
Determine o número de linhas afetadas pela operação. Se 1 linha for afetada, isso significa que a operação atual foi bem-sucedida e outras operações de dados podem ser executadas.
Se 0 linhas forem afetadas, significa que a solicitação é repetida e o sucesso é retornado diretamente.
O principal a ter em atenção é que esta solução se limita ao caso especial em que a tabela a atualizar tem um campo de estado, e o campo de estado só precisa de ser atualizado, nem todos os cenários são aplicáveis.

Adicionar bloqueio distribuído

Na verdade, adicionar um índice exclusivo ou adicionar uma tabela antipesada introduzida anteriormente é essencialmente usar o bloqueio distribuído do banco de dados, que também é um tipo de bloqueio distribuído. Mas como o desempenho do bloqueio distribuído do banco de dados não é muito bom, podemos usar redis ou zookeeper.

Tendo em vista que os centros de configuração distribuídos de muitas empresas mudaram para apollo ou nacos, e o zookeeper raramente é usado, usaremos o redis como exemplo para introduzir bloqueios distribuídos.

Atualmente, existem três maneiras principais de realizar o bloqueio distribuído de redis:

Comando setNx
Comando set
Redission framework
Cada esquema tem suas próprias vantagens e desvantagens. Não vou falar sobre os detalhes específicos da implementação. Amigos que estiverem interessados ​​podem me adicionar ao WeChat e conversar comigo em particular.

O fluxograma específico é o seguinte:

Etapas específicas:

Quando o usuário iniciar uma solicitação pelo navegador, o servidor coletará os dados e gerará o código do número do pedido como único campo comercial.
Use o comando set do redis para definir o código do pedido no redis e definir o período de tempo limite ao mesmo tempo.
Julgando se a configuração foi bem-sucedida, se a configuração for bem-sucedida, significa que é a primeira solicitação e a operação de dados é executada.
Se a configuração falhar, significa que a solicitação é repetida e retorna sucesso diretamente.
Atenção especial deve ser dada a: Os bloqueios distribuídos devem definir um tempo de expiração razoável.Se a configuração for muito curta, as solicitações repetidas não podem ser efetivamente evitadas. Se a configuração for muito longa, o espaço de armazenamento do redis pode ser desperdiçado, o que precisa ser determinado de acordo com a situação comercial real.

obter token

Além dos esquemas acima, existe um último esquema usando tokens. Essa solução é um pouco diferente de todas as soluções anteriores, exigindo duas solicitações para concluir uma operação comercial.

A primeira solicitação para obter o token
e a segunda solicitação trazem esse token para concluir a operação comercial.
O fluxograma específico é o seguinte:

O primeiro passo é obter o token primeiro.

O segundo passo é fazer operações comerciais específicas.

Etapas específicas:

Quando um usuário visita uma página, o navegador inicia automaticamente uma solicitação de token.
O servidor gera um token, salva-o no redis e o devolve ao navegador.
O token é carregado quando o usuário inicia uma solicitação por meio do navegador.
Consulte se o token existe no redis. Se não existir, significa que é a primeira solicitação e, em seguida, execute as operações de dados subsequentes.
Se existir, significa que a requisição é repetida, e retornará diretamente com sucesso.
No redis, o token será excluído automaticamente após o tempo de expiração.
A propósito, há realmente uma diferença entre design antipesado e design idempotente. O design antiduplicação é principalmente para evitar a duplicação de dados e não há muitos requisitos para o retorno da interface. Além de evitar dados duplicados, o design idempotente também requer que cada requisição retorne o mesmo resultado.

O esquema acima é projetado para idempotência.

Se for um projeto antipesado, o fluxograma pode ser alterado da seguinte forma:

Atenção especial é necessária: o token deve ser globalmente único.

Acho que você gosta

Origin blog.csdn.net/qq798280904/article/details/130722335
Recomendado
Clasificación