Considere um cenário com um banco de dados MySQL que suporta uma aplicação de negócios de recarga e pagamento. Um problema recorrente é a inserção aparentemente bem-sucedida de um pedido no banco de dados, mas que após uma falha no processo de pagamento (como com o WeChat Pay), o registro não é encontrado ou foi revertido. A raiz do problema está no gerenciamento manual de transações usando DataSourceTransactionManager e em como os métodos de persistência são invocados.
Fluxo de Operação e Análise do Problema
O processo típico envolve: 1) Inserir um pedido principle, 2) Consultar informações da estação de recarga, 3) Inserir um pedido filho, 4) Registrar operações. O código pode se assemelhar a:
TransactionStatus txStatus = transactionManager.getTransaction(transactionDefinition);
try {
Long pedidoId = registrarPedido(requisicao);
// ... outras operações de banco de dados ...
transactionManager.commit(txStatus);
} catch (Exception e) {
if (txStatus != null && !txStatus.isCompleted()) {
transactionManager.rollback(txStatus);
}
// Tratar falha no pagamento...
}
Ao inspecionar o banco de dados após uma exceção, o pedido pode estar lá, mesmo após o rollback. Isso indica que a operação registrarPedido(requisicao) não estava vinculada à transação gerenciada por txStatus.
Causas Raiz da Ineficácia da Transação
1. Auto-invocação e Perda do Proxy AOP: Se registrarPedido for um método anotado com @Transactional dentro do mesmo bean, a chamada via this.registrarPedido(...) contorna o proxy Spring AOP. Assim, a anotação @Transactional é ignorada, e o método executa com uma configuração de transação padrão (possivelmente auto-commit), desconectada da transação manual gerenciada.
2. Configuração do Banco de Dados: Verificar a variável autocommit do MySQL. Se estiver como ON e o código não explicitra transações, cada operação de escrita é confirmada imediatamente.
Abordagem Correta: Persistência Direta e Controle Explícito
A solução é garantir que a operação de persistência utilize a mesma conexão gerenciada pela transação manual. Em vez de chamar um método de serviço encapsulado, invoque diretamente os métodos do Mapper.
TransactionStatus txStatus = transactionManager.getTransaction(transactionDefinition);
try {
// Inserção direta via mapper, vinculada à transação corrente
PedidoEntity pedido = new PedidoEntity();
// ... preencher campos ...
pedidoMapper.insert(pedido); // Operação do MyBatis
Long pedidoId = pedido.getId();
// Outras operações...
transactionManager.commit(txStatus);
} catch (Exception e) {
// ...
transactionManager.rollback(txStatus);
}
Esta abordagem contorna o proxy Spring e garante que a inserção use a mesma conexão da transação manual. A tabela resume a mudança:
| Operação | Implementação Problemática | Correção | Motivo |
|---|---|---|---|
| Inserir Pedido | this.registrarPedido(entity) |
pedidoMapper.insert(entity) |
Evita proxy AOP e garante uso da conexão da transação |
| Atualizar Pedido | this.atualizarPedido(entity) |
pedidoMapper.updateById(entity) |
Same rationale |
O Problema Sutil da Propagação em Alta Concorrência
Em cenários de alta concorrência, problemas podem persistir mesmo com o código corrigido acima. Se o método que contém a transação manual for, ele mesmo, invocado dentro de uma transação externa (por exemplo, a partir de outro serviço Spring), a transação manual pode não ser totalmente isolada.
A propagação padrão PROPAGATION_REQUIRED faz com que a transação "interna" participe da transação externa existente. Portanto, se a transação externa for revertida, todas as operações realizadas dentro dela, incluindo as da sua transação manual "nova", também serão revertidas, mesmo que você tenha chamado commit explicitamente.
Solução: Propagação REQUIRES_NEW
Para garantir isolamento real, a transação interna deve suspender qualquer transação existente e iniciar uma completamente nova. Isso é feito configurando a propagação como REQUIRES_NEW.
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus statusExclusiva = transactionManager.getTransaction(def);
try {
// Todas as operações de banco de dados aqui ocorrem em uma transação
// completamente nova e isolada.
pedidoMapper.insert(pedido);
// ...
transactionManager.commit(statusExclusiva);
} catch (Exception e) {
transactionManager.rollback(statusExclusiva);
throw e; // Repassar a exceção para a lógica de tratamento externa
}
Com PROPAGATION_REQUIRES_NEW, a transação aninhada é fisicamente independente. Assim, um rollback da transação externa não afetará as operações já confirmadas por esta transação exclusiva. Esta técnica resolve o problema de ordens "fantasmas" em transações complexas e de alta concorrência.