← Назад к вопросам

Как реализуешь двухфазный коммит в транзакционном методе?

1.0 Junior🔥 91 комментариев
#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ

Как реализуешь двухфазный коммит в транзакционном методе

Двухфазный коммит (2PC) - это паттерн для обеспечения консистентности в распределённых системах с несколькими БД.

1. Что такое двухфазный коммит

Фаза 1: PREPARE (голосование)
  - Координатор спрашивает у всех участников: готовы ли вы?
  - Каждый выполняет транзакцию и отвечает YES или NO
  - Но НЕ коммитит ещё!

Фаза 2: COMMIT (финализация)
  - Если все ответили YES -> COMMIT всем
  - Если хотя бы один NO -> ROLLBACK всем
  - Гарантируется: ВСЕ коммитят или ВСЕ откатывают

2. Классическая реализация - XA транзакции

@Service
public class TransferService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private AccountRepository accountRepository;
    
    // XA transaction - двухфазный коммит на БД уровне
    @Transactional(propagation = Propagation.REQUIRED)
    public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) {
        // PHASE 1: PREPARE
        User fromUser = userRepository.findById(fromUserId).orElseThrow();
        User toUser = userRepository.findById(toUserId).orElseThrow();
        
        // Проверяем что можем выполнить
        if (fromUser.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException("Not enough money");
        }
        
        // Выполняем операции (но не коммитим)
        fromUser.setBalance(fromUser.getBalance().subtract(amount));
        toUser.setBalance(toUser.getBalance().add(amount));
        
        userRepository.save(fromUser);
        userRepository.save(toUser);
        
        // На выходе из метода
        // PHASE 2: COMMIT (если всё ок) или ROLLBACK (если ошибка)
    }
}

3. Несколько БД - явная 2PC реализация

@Service
public class CrossDatabaseTransferService {
    
    @Autowired
    private PrimaryDataSource primaryDataSource;  // БД 1
    
    @Autowired
    private SecondaryDataSource secondaryDataSource;  // БД 2
    
    public void transferAcrossDbases(Long userId, Long accountId, BigDecimal amount) {
        Connection primaryConn = null;
        Connection secondaryConn = null;
        
        try {
            // PHASE 1: PREPARE
            primaryConn = primaryDataSource.getConnection();
            secondaryConn = secondaryDataSource.getConnection();
            
            // Начинаем транзакции
            primaryConn.setAutoCommit(false);
            secondaryConn.setAutoCommit(false);
            
            // БД 1: уменьшить баланс пользователя
            String sql1 = "UPDATE users SET balance = balance - ? WHERE id = ?";
            PreparedStatement ps1 = primaryConn.prepareStatement(sql1);
            ps1.setBigDecimal(1, amount);
            ps1.setLong(2, userId);
            int rows1 = ps1.executeUpdate();
            
            if (rows1 == 0) {
                throw new UserNotFoundException("User not found");
            }
            
            // БД 2: увеличить баланс счёта
            String sql2 = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
            PreparedStatement ps2 = secondaryConn.prepareStatement(sql2);
            ps2.setBigDecimal(1, amount);
            ps2.setLong(2, accountId);
            int rows2 = ps2.executeUpdate();
            
            if (rows2 == 0) {
                throw new AccountNotFoundException("Account not found");
            }
            
            // PHASE 2: COMMIT
            primaryConn.commit();
            secondaryConn.commit();
            
            System.out.println("Transfer completed successfully");
            
        } catch (Exception e) {
            // ROLLBACK - откатываем обе транзакции
            try {
                if (primaryConn != null) primaryConn.rollback();
                if (secondaryConn != null) secondaryConn.rollback();
            } catch (SQLException rollbackError) {
                System.err.println("Rollback failed: " + rollbackError);
            }
            throw new TransactionFailedException("Transfer failed: " + e.getMessage(), e);
        } finally {
            // Закрыть соединения
            try {
                if (primaryConn != null) primaryConn.close();
                if (secondaryConn != null) secondaryConn.close();
            } catch (SQLException e) {
                System.err.println("Failed to close connection: " + e);
            }
        }
    }
}

4. Saga Pattern - альтернатива 2PC

Для микросервисов часто используют Saga вместо 2PC:

@Service
public class OrderSagaService {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private InventoryService inventoryService;
    
    @Transactional
    public void createOrderWithSaga(OrderRequest request) {
        // Шаг 1: Создаём заказ
        Order order = orderService.createOrder(request);
        
        try {
            // Шаг 2: Берём из инвентаря
            inventoryService.reserve(order.getItems());
            
            // Шаг 3: Обрабатываем платёж
            paymentService.charge(order.getId(), order.getAmount());
            
            // Всё успешно - коммитим
            order.setStatus(OrderStatus.CONFIRMED);
            orderService.save(order);
            
        } catch (PaymentFailedException e) {
            // Компенсирующие транзакции - откатываем
            inventoryService.release(order.getItems());
            order.setStatus(OrderStatus.FAILED);
            orderService.save(order);
            throw e;
        } catch (InventoryException e) {
            // Откатываем заказ
            orderService.cancel(order.getId());
            throw e;
        }
    }
}

5. JTA (Java Transaction API) - стандартный 2PC

@Service
public class JTATransactionService {
    
    @Autowired
    private UserTransaction userTransaction;  // JTA
    
    @Autowired
    private UserRepository userRepository;
    
    public void transferWithJTA(Long fromUserId, Long toUserId, BigDecimal amount) 
            throws Exception {
        
        // PHASE 1: BEGIN
        userTransaction.begin();
        
        try {
            // Выполняем операции
            User fromUser = userRepository.findById(fromUserId).orElseThrow();
            User toUser = userRepository.findById(toUserId).orElseThrow();
            
            fromUser.setBalance(fromUser.getBalance().subtract(amount));
            toUser.setBalance(toUser.getBalance().add(amount));
            
            userRepository.save(fromUser);
            userRepository.save(toUser);
            
            // PHASE 2: COMMIT
            userTransaction.commit();
            
        } catch (Exception e) {
            // ROLLBACK
            userTransaction.rollback();
            throw e;
        }
    }
}

6. Аннотация @Transactional - автоматический 2PC

@Service
public class SimpleTransactionService {
    
    // Spring автоматически управляет 2PC!
    @Transactional
    public void complexOperation(Long id1, Long id2) {
        // PHASE 1: PREPARE
        Entity entity1 = repository1.findById(id1).orElseThrow();
        Entity entity2 = repository2.findById(id2).orElseThrow();
        
        // Модифицируем
        entity1.update();
        entity2.update();
        
        // Сохраняем (нет реального коммита)
        repository1.save(entity1);
        repository2.save(entity2);
        
        // PHASE 2: COMMIT (автоматически при выходе из метода)
        // PHASE 2: ROLLBACK (если Exception)
    }
}

7. Таблица сравнения подходов

ПодходПростотаЗадержкаНадёжностьUse case
2PC (XA)СредняяВысокаяВысокаяНесколько БД, needs consistency
SagaВысокаяНизкаяСредняяМикросервисы, eventual consistency
@TransactionalНизкаяСредняяВысокаяSpring приложение, одна/две БД
Ручное управлениеСложнаяНизкаяЗависит от кодаОчень специфичные случаи

8. Best Practices

  1. Используй @Transactional - Spring управляет 2PC для тебя
  2. Для микросервисов - используй Saga вместо 2PC
  3. Помни про deadlocks - долгие 2PC блокируют ресурсы
  4. Тестируй failure scenarios - откаты должны работать
  5. Логируй все операции - для аудита и восстановления
  6. Используй timeouts - не зависай на вечность
  7. Избегай вложенных транзакций - усложняет логику

Кратко: Двухфазный коммит гарантирует 'all or nothing' в распределённых системах. Spring с @Transactional реализует его автоматически. Для микросервисов лучше использовать Saga Pattern.