8.1 IDs de transação
É designado como XID ou TxID (se houver alguma diferença, diga-me). Timestamps podem ser usados como TxID, que podem jogar a favor se quisermos restaurar todas as ações para algum ponto no tempo. O problema pode surgir se o registro de data e hora não for granular o suficiente - então as transações podem obter o mesmo ID.
Portanto, a opção mais confiável é gerar IDs de produto UUID exclusivos. Em Python isso é muito fácil:
>>> import uuid
>>> str(uuid.uuid4())
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0'
>>> str(uuid.uuid4())
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba'
Há também uma opção para fazer o hash de um conjunto de dados que definem a transação e usar esse hash como o TxID.
8.2 Novas tentativas
Se sabemos que uma determinada função ou programa é idempotente, isso significa que podemos e devemos tentar repetir sua chamada em caso de erro. E só temos que estar preparados para o fato de que alguma operação dará um erro - visto que os aplicativos modernos são distribuídos pela rede e pelo hardware, o erro não deve ser considerado uma exceção, mas a norma. O erro pode ocorrer devido a uma falha no servidor, erro de rede, congestionamento de aplicativos remotos. Como nosso aplicativo deve se comportar? Isso mesmo, tente repetir a operação.
Como um trecho de código pode dizer mais do que uma página inteira de palavras, vamos usar um exemplo para entender como o mecanismo de repetição ingênuo deveria funcionar idealmente. Vou demonstrar isso usando a biblioteca Tenacity (é tão bem projetada que, mesmo que você não planeje usá-la, o exemplo deve mostrar como você pode projetar o mecanismo de recorrência):
import logging
import random
import sys
from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential, retry_if_exception_type, before_log
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)
@retry(
stop=(stop_after_delay(10) | stop_after_attempt(5)),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(IOError),
before=before_log(logger, logging.DEBUG)
)
def do_something_unreliable():
if random.randint(0, 10) > 1:
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "Awesome sauce!"
print(do_something_unreliable.retry.statistics)
> Por via das dúvidas, direi: \@retry(...) é uma sintaxe especial do Python chamada "decorador". É apenas uma função retry(...) que envolve outra função e faz algo antes ou depois de ser executada.
Como podemos ver, as novas tentativas podem ser projetadas de forma criativa:
- Você pode limitar as tentativas por tempo (10 segundos) ou número de tentativas (5).
- Pode ser exponencial (ou seja, 2 ** algum número crescente n ). ou de alguma outra forma (por exemplo, fixo) para aumentar o tempo entre as tentativas separadas. A variante exponencial é chamada de "colapso de congestionamento".
- Você pode tentar novamente apenas para certos tipos de erros (IOError).
- Novas tentativas podem ser precedidas ou concluídas por algumas entradas especiais no log.
Agora que concluímos o curso Young Fighter e conhecemos os blocos de construção básicos que precisamos para trabalhar com transações no lado do aplicativo, vamos nos familiarizar com dois métodos que nos permitem implementar transações em sistemas distribuídos.
8.3 Ferramentas avançadas para amantes de transações
Darei apenas definições bastante gerais, já que este tópico merece um grande artigo separado.
Compromisso de duas fases (2pc) . 2pc tem duas fases: uma fase de preparação e uma fase de confirmação. Durante a fase de preparação, todos os microsserviços serão solicitados a se preparar para algumas alterações de dados que podem ser feitas atomicamente. Quando todos estiverem prontos, a fase de confirmação fará as alterações reais. Para coordenar o processo, é necessário um coordenador global, que bloqueia os objetos necessários - ou seja, eles ficam inacessíveis para alterações até que o coordenador os desbloqueie. Se um determinado microsserviço não estiver pronto para alterações (por exemplo, não responder), o coordenador abortará a transação e iniciará o processo de rollback.
Por que esse protocolo é bom? Ele fornece atomicidade. Além disso, garante o isolamento na hora de escrever e ler. Isso significa que as alterações em uma transação não são visíveis para outras até que o coordenador confirme as alterações. Mas essas propriedades também têm uma desvantagem: como esse protocolo é síncrono (bloqueio), ele torna o sistema mais lento (apesar do fato de a própria chamada RPC ser bastante lenta). E, novamente, existe o perigo de bloqueio mútuo.
Saga . Nesse padrão, uma transação distribuída é executada por transações locais assíncronas em todos os microsserviços associados. Os microsserviços se comunicam entre si por meio de um barramento de eventos. Se algum microsserviço falhar ao concluir sua transação local, outros microsserviços realizarão transações de compensação para reverter as alterações.
A vantagem do Saga é que nenhum objeto é bloqueado. Mas há, é claro, desvantagens.
Saga é difícil de depurar, especialmente quando há muitos microsserviços envolvidos. Outra desvantagem do padrão Saga é que ele não possui isolamento de leitura. Ou seja, se as propriedades indicadas no ACID são importantes para nós, o Saga não é muito adequado para nós.
O que vemos na descrição dessas duas técnicas? O fato de que em sistemas distribuídos, a responsabilidade pela atomicidade e isolamento é da aplicação. A mesma coisa acontece ao usar bancos de dados que não fornecem garantias ACID. Ou seja, coisas como resolução de conflitos, rollbacks, commits e liberação de espaço recaem sobre os ombros do desenvolvedor.
8.4 Como sei quando preciso de garantias ACID?
Quando há uma alta probabilidade de que um determinado conjunto de usuários ou processos trabalhe simultaneamente nos mesmos dados .
Desculpe a banalidade, mas um exemplo típico são as transações financeiras.
Quando a ordem em que as transações são executadas importa.
Imagine que sua empresa está prestes a mudar do mensageiro FunnyYellowChat para o mensageiro FunnyRedChat, porque o FunnyRedChat permite que você envie gifs, mas o FunnyYellowChat não. Mas você não está apenas mudando o mensageiro - você está migrando a correspondência da sua empresa de um mensageiro para outro. Você faz isso porque seus programadores estavam com preguiça de documentar programas e processos em algum lugar centralizado e, em vez disso, publicaram tudo em diferentes canais no messenger. Sim, e seus vendedores publicaram os detalhes das negociações e acordos no mesmo local. Resumindo, toda a vida da sua empresa está aí, e como ninguém tem tempo de transferir tudo para um serviço de documentação, e a busca por mensageiros instantâneos funciona bem, você decidiu em vez de limpar os escombros simplesmente copiar todos os mensagens para um novo local. A ordem das mensagens é importante
A propósito, para correspondência em um mensageiro, a ordem geralmente é importante, mas quando duas pessoas escrevem algo no mesmo bate-papo ao mesmo tempo, geralmente não é tão importante qual mensagem aparecerá primeiro. Portanto, para esse cenário específico, o ACID não seria necessário.
Outro exemplo possível é a bioinformática. Não entendo nada disso, mas presumo que a ordem seja importante ao decifrar o genoma humano. No entanto, ouvi dizer que os bioinformáticos geralmente usam algumas de suas ferramentas para tudo - talvez eles tenham seus próprios bancos de dados.
Quando você não pode fornecer a um usuário ou processar dados obsoletos.
E novamente - transações financeiras. Para ser sincero, não consegui pensar em nenhum outro exemplo.
Quando as transações pendentes estão associadas a custos significativos. Imagine os problemas que podem surgir quando um médico e uma enfermeira atualizam o registro de um paciente e apagam as alterações um do outro ao mesmo tempo, porque o banco de dados não pode isolar as transações. O sistema de saúde é outra área, além da financeira, onde as garantias do ACID tendem a ser críticas.
8.5 Quando não preciso do ACID?
Quando os usuários atualizam apenas alguns de seus dados privados.
Por exemplo, um usuário deixa comentários ou notas adesivas em uma página da web. Ou edita dados pessoais em uma conta pessoal com um provedor de qualquer serviço.
Quando os usuários não atualizam os dados, mas apenas complementam com novos (acrescentar).
Por exemplo, um aplicativo de corrida que salva os dados de suas corridas: quanto você correu, a que horas, percurso, etc. Cada nova execução contém novos dados e os antigos não são editados. Talvez, com base nos dados, você obtenha análises - e apenas os bancos de dados NoSQL são bons para esse cenário.
Quando a lógica de negócios não determina a necessidade de uma determinada ordem na qual as transações são realizadas.
Provavelmente, para um blogueiro do Youtube que arrecada doações para a produção de novo material durante a próxima transmissão ao vivo, não é tão importante quem, quando e em que ordem, jogou dinheiro para ele.
Quando os usuários permanecerão na mesma página da Web ou janela do aplicativo por vários segundos ou até minutos e, portanto, de alguma forma, verão dados obsoletos.
Teoricamente, trata-se de qualquer mídia de notícias online ou do mesmo Youtube. Ou "Habr". Quando não importa para você que transações incompletas possam ser armazenadas temporariamente no sistema, você pode ignorá-las sem nenhum dano.
Se você estiver agregando dados de várias fontes e dados atualizados com alta frequência - por exemplo, dados sobre a ocupação de vagas de estacionamento em uma cidade que muda pelo menos a cada 5 minutos, então, em teoria, não será um grande problema para você se em algum momento a transação de um dos estacionamentos não for realizada. Embora, é claro, dependa do que exatamente você deseja fazer com esses dados.
GO TO FULL VERSION