O seu feed delta perde commits tardios: construa um cursor fiável
Uma linha chegou tarde porque a marca de água não era uma fronteira de commit. Percorra a falha e depois defina cobertura, fronteira, retenção e uma retoma segura.
Eis uma falha que vi acontecer duas vezes, em dois motores diferentes. Uma exportação corre às 10:00 e registra a marca temporal de última modificação mais alta que viu. Uma transação que começou às 09:58 continua aberta, faz commit às 10:03 e traz uma marca temporal do momento em que começou. A exportação seguinte pede apenas linhas mais novas do que 10:00 e nunca a vê. O tamanho da transferência era excelente e os dados estavam errados.
Percorra a exportação que perdeu uma linha
Siga a ordem dos acontecimentos, não o SQL. A aplicação escreve o tempo de modificação da linha quando a transação começa, não quando faz commit. O exportador lê uma marca de água de relógio de parede enquanto essa transação ainda está aberta. Depois a transação faz commit com um tempo de modificação anterior ao da marca guardada. O lote seguinte do consumidor começa depois da marca, por isso a linha fica atrás do cursor e nunca mais será selecionada.
Nada registra um erro, porque nada falhou. É isso que torna esta classe de erros cara: o feed delta é rápido, as contagens de linhas parecem plausíveis e a linha em falta aparece semanas depois como um ticket de reconciliação que ninguém consegue reproduzir.
Os números de sequência têm a mesma forma de defeito por outra razão. Um ID monotónico diz-lhe a ordem em que os valores foram atribuídos, não a ordem em que as transações fizeram commit. Uma transação que apanhou um número baixo pode fazer commit depois de uma que apanhou um número alto, por isso um cursor baseado na ordem de atribuição também pode deixar um buraco.
Cinco promessas que o feed tem de cumprir
Antes de escolher um transporte, peça ao sistema de origem um contrato com cinco cláusulas. Se faltar alguma, tem uma heurística e não um feed de alterações.
- Cobertura: inserções, atualizações e eliminações, cada uma com a chave estável necessária para a aplicar.
- Fronteira: o que significa um valor de cursor e o que contém um lote coerente.
- Consistência: como se leem tabelas relacionadas sem misturar estados de origem incompatíveis.
- Retenção: quanto tempo um consumidor desligado pode retomar a partir da sua posição guardada.
- Reinicialização: um instantâneo completo suportado para quando essa posição já não é válida.
Um mecanismo suportado pode satisfazer as cinco. A documentação do SQL Server sobre o rastreio de alterações é um exemplo publicado concreto: define uma versão mínima válida, registra eliminações além das alterações e descreve uma transação de instantâneo que abrange validação, captura de versão e as consultas de alterações relacionadas. É um exemplo do que exigir a uma origem, não uma sintaxe que se cole noutro motor.
Desenhe o ponto de controlo como parte do pipeline
A posição guardada do consumidor é a verdadeira máquina de estados, por isso escreva-a como tal. O cursor só pode avançar através do fluxo de trabalho que já aplicou o lote e confirmou a transação de destino.
cursor = durable_checkpoint() # last fully applied position
batch = extract(cursor, upper_bound) # one consistent batch
apply(batch) # idempotent at the target
commit()
checkpoint(batch.upper_bound) # only now
Se a entrega falhar a meio, reconcilie esse mesmo lote em vez de inventar uma nova posição de origem. Reextrair com um limite superior mais recente é a forma como uma aplicação parcial se torna num buraco permanente. A regra do espelho também vale do outro lado: a aplicação idempotente no destino evita duplicados e não faz nada por uma alteração que a extração nunca viu.
As eliminações precisam de registos, não de conclusões
Comparar linhas que ainda existem não pode dizer a um consumidor que chaves desapareceram. Uma origem que nunca registra eliminações deixa-lhe duas opções imperfeitas: manter um histórico de chaves suficientemente longo para comparar, ou executar uma reconciliação completa explícita segundo um calendário. Ambas são decisões de desenho com um custo, e nenhuma existe se ninguém a assumir.
A retenção é o mesmo tipo de decisão. Todo o feed de alterações suportado tem uma janela; declará-la no contrato e avisar quando o ponto de controlo de um consumidor se aproxima dela. Um consumidor que acorda após uma interrupção de duas semanas deve ler um erro claro "é necessária reinicialização" em vez de receber silenciosamente um lote parcial.
Depois meça se o delta valeu a pena
Voltar a comparar cada chave e hash às vezes é correto, e vale a pena saber quanto custa. Continua a transferir informação sobre cada linha, os hash não lhe dão um instantâneo coerente entre tabelas, e um esquema de hash traz as suas próprias perguntas sobre representação e colisões. Menos bytes com uma fronteira verificável é o objetivo; menos bytes que deixam cair silenciosamente uma alteração confirmada não é delta nenhum.
Uma coluna de última modificação é uma pista. Um cursor que sobrevive a um commit tardio, a uma eliminação e a uma longa interrupção é um contrato.