Tue Dec 30 2025
Распределённые транзакции в микросервисах
Распределённые транзакции — сложная проблема в микросервисной архитектуре. В монолите вы полагаетесь на ACID-транзакции базы данных, но в распределённой системе всё сложнее. Когда данные разбросаны по нескольким сервисам, как обеспечить атомарность операций? В этой статье рассмотрим основные подходы: 2PC, Saga и Outbox Pattern.
Проблема
Сервис A Сервис B Сервис C
│ │ │
├─── Транзакция ───┼───────────┤
│ (ACID) │ │
│ │ │
└──────────────────┴───────────┘
Проблема: как обеспечить атомарность между сервисами?
Two-Phase Commit (2PC)
2PC — классический протокол для распределённых транзакций. Координатор управляет двумя фазами: подготовка и коммит.
Phase 1: Prepare
┌──────────────────────────────┐
│ Coordinator ── A: Prepare? │
│ Coordinator ── B: Prepare? │
│ Coordinator ── C: Prepare? │
│ │
│ A ── Ready │
│ B ── Ready │
│ C ── Ready │
└──────────────────────────────┘
Phase 2: Commit
┌────────────────────────────┐
│ Coordinator ── A: Commit │
│ Coordinator ── B: Commit │
│ Coordinator ── C: Commit │
│ │
│ A ── Done │
│ B ── Done │
│ C ── Done │
└────────────────────────────┘
Недостатки 2PC
- Блокировка ресурсов
- Coordinator — single point of failure
- Сложность реализации
Saga Pattern
┌───────────────────────────────────────┐
│ Order Service │
│ 1. createOrder() → Создание заказа │
│ 2. compensate() → Отмена заказа │
├───────────────────────────────────────┤
│ Payment Service │
│ 3. charge() → Оплата │
│ 4. compensate() → Возврат средств │
├───────────────────────────────────────┤
│ Inventory Service │
│ 5. reserve() → Резервирование │
│ 6. compensate() → Снятие резерва │
├───────────────────────────────────────┤
│ Shipping Service │
│ 7. ship() → Доставка │
│ 8. compensate() → Отмена доставки │
└───────────────────────────────────────┘
Choreography-based Saga
// Сервис заказов
async function createOrder(orderData) {
const order = await Order.create({ ...orderData, status: 'PENDING' });
await MessageBroker.publish('order.created', {
orderId: order.id,
items: order.items
});
return order;
}
// Сервис оплаты
async function handleOrderCreated(event) {
await Payment.charge(event.orderId, event.amount);
await MessageBroker.publish('payment.completed', { orderId: event.orderId });
}
async function handlePaymentFailed(event) {
await Order.update(event.orderId, { status: 'CANCELLED' });
await MessageBroker.publish('order.cancelled', { orderId: event.orderId });
}
Orchestration-based Saga
// Orchestrator
class OrderSaga {
async execute(orderData) {
try {
// Шаг 1: Создать заказ
const order = await OrderService.create({ ...orderData, status: 'PENDING' });
// Шаг 2: Обработать оплату
await PaymentService.charge(order.id, order.amount);
// Шаг 3: Зарезервировать товар
await InventoryService.reserve(order.items);
// Шаг 4: Запустить доставку
await ShippingService.ship(order.id);
// Шаг 5: Обновить статус
await OrderService.update(order.id, { status: 'COMPLETED' });
} catch (error) {
// Compensating transactions
await this.compensate(error);
}
}
async compensate(error) {
const completedSteps = error.completedSteps || [];
for (const step of completedSteps.reverse()) {
try {
await step.compensate();
} catch (e) {
// Логирование, manual intervention
await AlertService.notify('compensation_failed', { error: e });
}
}
}
}
Outbox Pattern
┌───────────────────────────────────────────────┐
│ Application │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Transaction │ │ Outbox │ │
│ │ (БД) │ ─── │ Table │ │
│ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌─────────────────────────────────────────┐ │
│ │ Transaction Log Miner (CDC/Broker) │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
└───────────────────────┼───────────────────────┘
│
┌────────────┴────────────┐
┌──────────────┐ ┌──────────────┐
│ Service A │ │ Service B │
└──────────────┘ └──────────────┘
Реализация
// Модель
class Order extends Model {
static tableName = 'orders';
static jsonSchema = {
type: 'object',
required: ['items'],
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['PENDING', 'PAID', 'SHIPPED', 'CANCELLED'] },
items: { type: 'array' }
}
};
}
// Outbox table
class OutboxEvent extends Model {
static tableName = 'outbox';
static async append(aggregateType, aggregateId, type, payload) {
await this.query().insert({
aggregate_type: aggregateType,
aggregate_id: aggregateId,
type,
payload: JSON.stringify(payload),
created_at: new Date()
});
}
}
// Транзакция + outbox
async function createOrder(orderData) {
return db.transaction(async (trx) => {
// 1. Создать заказ
const order = await Order.query(trx).insert({
id: uuid(),
...orderData,
status: 'PENDING'
});
// 2. Записать в outbox
await OutboxEvent.query(trx).insert({
aggregate_type: 'order',
aggregate_id: order.id,
type: 'order.created',
payload: { orderId: order.id, items: order.items }
});
return order;
});
}
Eventual Consistency
┌───────────────────────────┐
│ Strong Consistency │
│ A ── B ── C ── Done │
│ | | | │
│ └────┴────┘ │
│ Всё или ничего │
├───────────────────────────┤
│ Eventual Consistency │
│ A ── B ── C ── Done │
│ │ │ │ │
│ └────┴────┘ │
│ Со временем согласуется │
└───────────────────────────┘
Выбор подхода
- 2PC: Несколько сервисов, строгая согласованность
- Saga (choreography): Независимые сервисы, простые сценарии
- Saga (orchestration): Сложные сценарии, явный контроль
- Outbox: Publish events из транзакций
Best Practices
- Idempotency — обработчики должны быть идемпотентными
- Compensation — всегда продумывайте откат
- Timeouts — устанавливайте разумные таймауты
- Monitoring — отслеживайте saga execution
- Manual intervention — планируйте ручное вмешательство
Заключение
Распределённые транзакции требуют иного подхода, чем монолитные ACID транзакции. Saga и Outbox — основные паттерны.