Estratégias Comuns para Transações Distribuídas
Existem diversas abordagens para lidar com transações distribuídas:
- Protocolos XA/JTA: Baseada em bancos de dados, requer suporte do fornecedor do banco de dados e pode ser implementada com componentes Java como o Atomikos.
- Verificação Assíncrona de Dados: Semelhante ao mecanismo de pagamento de plataformas como Alipay e WeChat Pay, que consultam o status do pagamento e realizam conciliações.
- Soluções com Mensagens Confiáveis (MQ): Ideal para cenários assíncronos, oferecendo grande generalidade e escalabilidade.
- Soluções Programáticas TCC (Try-Confirm-Cancel): Implementadas internamente por empresas como a Alibaba e Ant Financial (DTX).
Implementação com RabbitMQ
Conceito Geral
O objetivo é garantir a consistência dos dados em diferentes sistemas, com dois requisitos principais:
- Produção Confiável: Assegurar que as mensagens sejam enviadas com sucesso para o broker RabbitMQ.
- Consumo Confiável: Garantir que as mensagens sejam processadas corretamente após serem recebidas.
Após o envio de uma mensagem para o RabbitMQ, um callback é acionado para atualizar o status do envio. Para habilitar essa confirmação, configure o seguinte no seu application.properties (ou similar):
spring.rabbitmq.publisher-confirm-type=CORRELATED
Um exemplo de implementação em Java:
import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct;
@Component public class MessageSender {
private volatile boolean messageSentSuccessfully = false;
private final RabbitTemplate rabbitTemplate;
@Autowired
public MessageSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@PostConstruct
public void setupConfirmCallback() {
rabbitTemplate.setConfirmCallback((correlationId, ack, cause) -> {
if (!ack) {
// Lógica para tratamento de falha no envio
System.err.println("Falha ao enviar mensagem: " + cause);
} else {
// Lógica para tratamento de sucesso no envio
System.out.println("Mensagem enviada com sucesso.");
}
// Sinaliza que o processo de envio (e callback) foi concluído
messageSentSuccessfully = true;
});
}
public void sendMessage(String message) {
// Configura um CorrelationId para rastreamento se necessário
// CorrelationData correlationData = new CorrelationData("unique-message-id");
rabbitTemplate.convertAndSend("OrderDispatchExchange", null, message); //, correlationData);
System.out.println("Solicitação de envio de mensagem enviada.");
// Aguarda até que o callback seja executado para evitar o término prematuro do teste
// Em um ambiente de produção, o controle de fluxo seria diferente.
while (!messageSentSuccessfully) {
try {
Thread.sleep(100); // Pequena pausa para não consumir CPU excessivamente
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Thread interrompida durante espera.");
break;
}
}
messageSentSuccessfully = false; // Reseta para a próxima chamada
}
// Exemplo de uso em um teste
public void testMessageDispatch() {
sendMessage("Dados do pedido para processamento");
System.out.println("Teste de envio concluído.");
}
}
</div>**Observação:** Em caso de falhas na confirmação de recebimento ou atualização de status, uma estratégia adicional pode ser a verificação periódica de uma tabela de mensagens. Mensagens que não foram confirmadas após um tempo limite podem ser reenviadas.
### Idempotência
Parra garantir a idempotência, utilize identificadores únicos (como ID de transação ou dados de negócio específicos) para verificar se uma operação já foi processada. Em caso de duplicidade, a operação deve ser ignorada.
### Confirmação de Consumo (ACK/NACK)
Após o processamento bem-sucedido de uma mensagem, o consumidor deve enviar uma confirmação (ACK) para o RabbitMQ, permitindo que a mensagem seja removida da fila. Em caso de falha no processamento, o consumidor pode notificar o RabbitMQ para que a mensagem seja reenviada (NACK com `requeue=true`) ou descartada (NACK com `requeue=false`).
Para habilitar o reconhecimento manual das mensagens, configure:
spring.rabbitmq.listener.simple.acknowledge-mode=MANUAL
Exemplo de consumidor com ACK/NACK manual:
<div> ```
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumer {
// Contador de tentativas para demonstração (em produção, use persistência)
private int retryCount = 0;
private final int MAX_RETRIES = 3;
@RabbitListener(queues = "OrderDispatchQueue")
public void processOrderMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
try {
System.out.println("Recebida mensagem: " + message);
// --- Lógica de Processamento ---
// 1. Verificação de Idempotência: Use um ID de transação ou chave de negócio
// para garantir que a mesma mensagem não seja processada múltiplas vezes.
// Ex: if (isAlreadyProcessed(messageId)) { channel.basicAck(tag, false); return; }
// 2. Execução da lógica de negócio.
if (message.contains("pedido_falho")) { // Simula uma falha
throw new RuntimeException("Simulação de erro no processamento.");
}
System.out.println("Processamento da mensagem concluído.");
// --- Fim da Lógica de Processamento ---
// Confirma o processamento bem-sucedido
channel.basicAck(tag, false);
retryCount = 0; // Reseta a contagem de tentativas em caso de sucesso
} catch (Exception e) {
System.err.println("Erro ao processar mensagem: " + e.getMessage());
// Tenta reenviar a mensagem se o número de tentativas não foi excedido
if (retryCount < MAX_RETRIES) {
retryCount++;
System.out.println("Reenviando mensagem. Tentativa: " + retryCount);
// Rejeita a mensagem e a coloca de volta na fila para nova tentativa
channel.basicNack(tag, false, true);
} else {
// Se as tentativas falharem, descarta a mensagem e registra para análise manual
System.err.println("Número máximo de tentativas atingido. Descartando mensagem.");
// Envia para uma Dead Letter Queue (DLQ) ou registra para intervenção manual
channel.basicNack(tag, false, false);
// Em produção, acione um alerta ou mecanismo de notificação
}
}
}
}
Vantagens e Desvantagens
Vantagens:
- Alta Generalidade e Escalabilidade: Adequado para uma ampla gama de cenários e fácil de escalar.
- Solução Madura: O RabbitMQ é uma tecnologia estabelecida com uma comunidade ativa.
Desvantagens:
- Cenários Assíncronos: Inadequado para transações que exigem confirmação síncrona e rollback imediato.
- Latência: O processamento de mensagens pode ter um pequeno atraso, o que deve ser tolerável pela lógica de negócio.
Recomendação: Sempre que possível, evite transações distribuídas síncronas complexas. Prefira desacoplar processos não críticos e tratá-los de forma assíncrona usando filas de mensagens.