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

Что такое двухфазный commit?

1.7 Middle🔥 141 комментариев
#Базы данных и SQL

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

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

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

# Двухфазный commit (2PC — Two-Phase Commit)

Двухфазный commit — это распределённый протокол обеспечения атомарности транзакций в системах с несколькими базами данных или ресурсами. Гарантирует, что либо ВСЕ системы применят изменения, либо ВСЕ откатят.

Проблема: Распределённые транзакции

Приложение → БД1        БД2       БД3
             (PostgreSQL) (MySQL)  (Oracle)

Транзакция должна либо успешно завершиться
на ВСЕХ базах, либо откатиться везде.

Что, если:
- БД1 коммитила успешно
- БД2 упала перед коммитом
- БД3 заблокирована

→ Данные несогласованны!

Две фазы 2PC

Фаза 1: Prepare (Голосование)

Координатор                БД1              БД2              БД3
─────────────────────────────────────────────────────────────────
Пошлём подготовку
                    PREPARE         PREPARE         PREPARE
                    ↓               ↓               ↓
            Блокируем ресурсы  Блокируем ресурсы  Блокируем ресурсы
            Проверяем согласованность везде
            Ответ: YES или NO
                    ↑               ↑               ↑
            YES vote        YES vote        YES vote

Все БД готовы и обещают закоммитить

Фаза 2: Commit или Abort (Выполнение)

Если все БД ответили YES:

Координатор                БД1              БД2              БД3
─────────────────────────────────────────────────────────────────
Пошлём commit
                    COMMIT          COMMIT          COMMIT
                    ↓               ↓               ↓
            Применяем изменения везде
                    ↑               ↑               ↑
            ACK             ACK             ACK

Транзакция завершена на ВСЕХ базах

Если хотя бы одна БД ответила NO:

Координатор                БД1              БД2              БД3
─────────────────────────────────────────────────────────────────
Пошлём abort
                    ABORT           ABORT           ABORT
                    ↓               ↓               ↓
            Откатываем изменения везде
                    ↑               ↑               ↑
            ACK             ACK             ACK

Транзакция откачена везде

Пример: Перевод денег между банками

@Service
public class DistributedTransferService {
    
    @Autowired
    private JtaTransactionManager transactionManager;
    
    @Autowired
    private BankARepository bankA;
    
    @Autowired
    private BankBRepository bankB;
    
    // JTA обеспечивает 2PC
    @Transactional(propagation = Propagation.REQUIRED)
    public void transferMoneyBetweenBanks(
            Long accountA, 
            Long accountB, 
            BigDecimal amount) {
        
        try {
            // === ФАЗА 1: PREPARE ===
            // JTA транзакция начата
            
            // Снимаем деньги с первого банка
            Account a = bankA.findByIdForUpdate(accountA);
            if (a.getBalance().compareTo(amount) < 0) {
                throw new InsufficientFundsException();
            }
            a.setBalance(a.getBalance().subtract(amount));
            bankA.save(a); // Логируется в БД1
            
            // Пополняем счёт второго банка
            Account b = bankB.findByIdForUpdate(accountB);
            b.setBalance(b.getBalance().add(amount));
            bankB.save(b); // Логируется в БД2
            
            // === ФАЗА 2: COMMIT ===
            // Оба банка отвечают YES
            // JTA автоматически вызывает COMMIT на обеих БД
            
        } catch (Exception e) {
            // === ФАЗА 2: ABORT ===
            // Ошибка! JTA вызывает ABORT на обеих БД
            // Оба изменения откатываются
            throw e;
        }
    }
}

Реальный сценарий сбоя

Фаза 1: Prepare
─────────────────────────────────────────────────────
БД1: Блокировка + проверка → YES
БД2: Блокировка + проверка → YES
БД3: Блокировка + проверка → YES

Все готовы к коммиту!

Фаза 2: Commit
─────────────────────────────────────────────────────
БД1: COMMIT → ✓ Успех
БД2: COMMIT → ✗ ПАДЕНИЕ СЕРВЕРА!
БД3: COMMIT → ?

Что произойдёт:
1. БД1 уже закоммитила → данные применены
2. БД2 упала → НЕ закоммитила
3. БД3 ждёт инструкций

→ НЕСОГЛАСОВАННОСТЬ!

Защита: Благодаря логам и recovery:
- Когда БД2 восстановится, координатор пересылает
- команду COMMIT (которую БД2 не получила)
- БД2 восстанавливается и коммитит

Проблемы 2PC

1. Низкая производительность

// Все БД блокируют ресурсы до конца фазы 2
// Другие транзакции ждут

@Transactional
public void slowTransaction() {
    // Фаза 1: 100ms на подготовку
    // + сетевая задержка: 50ms × 3 БД = 150ms
    // + проверка согласованности: 200ms
    // = Всё заблокировано на 450ms
    // 
    // Если 1000 таких транзакций в сек:
    // Пропускная способность упадёт в 2-3 раза
}

2. Synchronous nature

Приложение ждёт ответа от ВСЕХ БД. Если одна медленная — все медленнее.

3. Сложность в микросервисах

2PC плохо масштабируется в системах с десятками микросервисов.

Альтернатива: Saga Pattern

Для микросервисов предпочитают Saga (асинхронная компенсация ошибок):

@Service
public class TransferSagaService {
    
    @Autowired
    private KafkaTemplate<String, Event> kafka;
    
    public void transferWithSaga(Long accountA, Long accountB, BigDecimal amount) {
        // Шаг 1: Снять деньги с А
        try {
            withdrawEvent = new MoneyWithdrawnEvent(accountA, amount);
            kafka.send("withdrawals", withdrawEvent);
            
            // Шаг 2: Пополнить Б
            depositEvent = new MoneyDepositedEvent(accountB, amount);
            kafka.send("deposits", depositEvent);
            
        } catch (Exception e) {
            // Компенсация: вернуть деньги на А
            compensateEvent = new MoneyReturnedEvent(accountA, amount);
            kafka.send("compensations", compensateEvent);
        }
    }
}

Spring Boot JTA пример

@Configuration
public class JtaConfig {
    
    @Bean
    public JtaTransactionManager transactionManager() {
        return new JtaTransactionManager();
    }
}

@Service
public class MultiDataSourceService {
    
    @Autowired
    private DataSource dataSource1; // БД1
    
    @Autowired
    private DataSource dataSource2; // БД2
    
    // 2PC автоматически управляет обеими БД
    @Transactional
    public void atomicOperation() {
        // INSERT в БД1
        // UPDATE в БД2
        // Commit или Abort на ОБЕИХ
    }
}

Когда использовать 2PC

Используй:

  • Критические финансовые операции
  • Трансграничные переводы
  • Операции с несколькими БД (редко)
  • Когда согласованность важнее производительности

Избегай:

  • Микросервисная архитектура → Saga
  • Высоконагруженные системы → Saga или Event sourcing
  • Когда требуется масштабируемость

Заключение

  • 2PC гарантирует атомарность распределённых транзакций
  • Две фазы: Prepare (проверка готовности) + Commit/Abort (выполнение)
  • Недостаток: Блокировка ресурсов → низкая производительность
  • Альтернатива: Saga pattern для асинхронных систем
  • В Java: JTA обеспечивает 2PC, но редко используется в modern stack
Что такое двухфазный commit? | PrepBro