Комментарии (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