Tratamento manual de transações e falha de persistência no Spring com MyBatis

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.

Tags: Spring Framework Transaction Management MyBatis MySQL Data Consistency

Publicado em 6-8 03:45 por Thomas