← Назад к вопросам
Для какой задачи может применяться Outbox
2.8 Senior🔥 141 комментариев
#REST API и микросервисы#Брокеры сообщений
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Паттерн Outbox (Исходящий ящик)
Определение
Outbox Pattern (Паттерн Исходящего ящика) — это архитектурный паттерн для обеспечения надёжной доставки сообщений в распределённых системах, особенно в микросервисной архитектуре. Он гарантирует, что сообщение о событии будет отправлено ровно один раз, даже при сбоях.
Проблема, которую решает Outbox
Сценарий без Outbox (опасный)
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private MessageQueue messageQueue;
public void createOrder(Order order) {
// Шаг 1: Сохраняем заказ в БД
orderRepository.save(order); // OK
// Шаг 2: Отправляем сообщение в очередь
messageQueue.send(new OrderCreatedEvent(order)); // ЧТО ЕСЛИ ПАДЁТ?
// Если здесь упадёт приложение:
// - Заказ сохранён
// - Сообщение НЕ отправлено
// - Другие микросервисы не узнают о заказе!
}
}
Проблемы:
- Потеря сообщения — если приложение падёт до отправки
- Несогласованность данных — БД и очередь в разных состояниях
- Дублирование — могут быть отправлены дубли при повторе
Решение: Outbox Pattern
Архитектура
// Шаг 1: Создаём таблицу OUTBOX в БД
@Entity
@Table(name = "outbox")
public class OutboxEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "aggregate_type")
private String aggregateType; // "Order", "Payment", etc.
@Column(name = "aggregate_id")
private String aggregateId; // ID заказа, платежа, и т.д.
@Column(name = "event_type")
private String eventType; // "OrderCreated", "OrderConfirmed", etc.
@Column(columnDefinition = "TEXT")
private String payload; // JSON с данными события
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "processed")
private boolean processed = false;
@Column(name = "processed_at")
private LocalDateTime processedAt;
}
// Шаг 2: Используем одну транзакцию для обоих операций
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OutboxRepository outboxRepository;
@Transactional
public void createOrder(Order order) {
// ОБЕ операции в одной транзакции БД
// 1. Сохраняем заказ
Order saved = orderRepository.save(order);
// 2. Создаём событие в OUTBOX (в той же транзакции)
OutboxEvent event = new OutboxEvent();
event.setAggregateType("Order");
event.setAggregateId(saved.getId().toString());
event.setEventType("OrderCreated");
event.setPayload(serializeOrder(saved));
event.setCreatedAt(LocalDateTime.now());
outboxRepository.save(event);
// Если здесь произойдёт ошибка:
// - Откатывается ВСЁ (заказ и событие)
// - Нет несогласованности!
}
private String serializeOrder(Order order) {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(order);
}
}
// Шаг 3: Отдельный процесс читает из OUTBOX и отправляет
@Component
public class OutboxPoller {
@Autowired
private OutboxRepository outboxRepository;
@Autowired
private MessageQueue messageQueue;
@Scheduled(fixedDelay = 1000) // Каждую секунду
public void pollOutbox() {
// Находим необработанные события
List<OutboxEvent> unprocessed = outboxRepository
.findByProcessedFalse();
for (OutboxEvent event : unprocessed) {
try {
// Отправляем в очередь
messageQueue.send(event);
// Отмечаем как обработанное
event.setProcessed(true);
event.setProcessedAt(LocalDateTime.now());
outboxRepository.save(event);
System.out.println("Event sent: " + event.getEventType());
} catch (Exception e) {
System.err.println("Failed to send event: " + e.getMessage());
// Повторим на следующей итерации
}
}
}
}
Полный пример: E-commerce система
// Таблица для хранения событий
CREATE TABLE outbox (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
aggregate_type VARCHAR(255) NOT NULL,
aggregate_id VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload LONGTEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
processed BOOLEAN DEFAULT FALSE,
processed_at TIMESTAMP NULL
);
// Order Service - создаёт заказ и событие
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OutboxRepository outboxRepository;
public void processOrder(OrderRequest request) {
// Создаём и сохраняем заказ
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setTotalAmount(request.getAmount());
order.setStatus(OrderStatus.PENDING);
Order savedOrder = orderRepository.save(order);
// Создаём событие
createOutboxEvent(
"Order",
savedOrder.getId().toString(),
"OrderCreated",
savedOrder
);
}
private void createOutboxEvent(String aggregateType,
String aggregateId,
String eventType,
Object payload) {
OutboxEvent event = new OutboxEvent();
event.setAggregateType(aggregateType);
event.setAggregateId(aggregateId);
event.setEventType(eventType);
event.setPayload(convertToJson(payload));
event.setCreatedAt(LocalDateTime.now());
outboxRepository.save(event);
}
private String convertToJson(Object object) {
try {
return new ObjectMapper().writeValueAsString(object);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// Outbox Processor - отправляет события
@Component
public class OutboxEventProcessor {
@Autowired
private OutboxRepository outboxRepository;
@Autowired
private RabbitTemplate rabbitTemplate; // или Kafka, или другая очередь
@Scheduled(fixedDelay = 1000, initialDelay = 5000)
public void processOutboxEvents() {
List<OutboxEvent> events = outboxRepository.findByProcessedFalse();
for (OutboxEvent event : events) {
try {
// Отправляем в RabbitMQ/Kafka
rabbitTemplate.convertAndSend(
"order-events", // exchange
event.getEventType(), // routing key
createMessage(event)
);
// Отмечаем как обработанное
event.setProcessed(true);
event.setProcessedAt(LocalDateTime.now());
outboxRepository.save(event);
log.info("Processed outbox event: {}", event.getId());
} catch (Exception e) {
log.error("Failed to process outbox event: {}", event.getId(), e);
// На следующей итерации повторим
}
}
}
private String createMessage(OutboxEvent event) {
return new MessageBuilder()
.withEventId(event.getId())
.withEventType(event.getEventType())
.withAggregateId(event.getAggregateId())
.withPayload(event.getPayload())
.build()
.toString();
}
}
// Consumer в другом микросервисе
@Service
public class PaymentService {
@RabbitListener(queues = "order-events")
public void handleOrderCreated(OrderCreatedEvent event) {
log.info("Received order created event: {}", event.getOrderId());
// Обрабатываем события
if ("OrderCreated".equals(event.getEventType())) {
processPayment(event);
}
}
private void processPayment(OrderCreatedEvent event) {
// Логика обработки платежа
log.info("Processing payment for order: {}", event.getOrderId());
}
}
Преимущества Outbox Pattern
1. Гарантирует доставку
// Даже если брокер упал, событие остаётся в OUTBOX
// Повторная попытка отправки гарантирована
2. Обеспечивает консистентность
// Либо ОБА успешны (заказ + событие),
// либо ОБА откатываются
// Никогда не будет несогласованности
3. Работает даже при сбоях
// Если сервис упал посередине:
// - На следующем старте процесс продолжит отправку
// - События, которые не были обработаны, будут отправлены
Альтернативы Outbox
1. CDC (Change Data Capture)
// Используем инструменты типа Debezium
// для отслеживания изменений в БД
// и автоматической отправки событий
2. Event Sourcing
// Храним полную историю событий
// вместо текущего состояния
Когда использовать Outbox
✅ Используй Outbox когда:
- Нужна надёжная доставка сообщений
- Работаешь с микросервисами
- БД и очередь должны быть в консистентном состоянии
- Нельзя потерять события
❌ Не нужен Outbox когда:
- Используешь одну БД (не распределённая система)
- Событие некритично (можно потерять)
- Потребитель сам получает данные из БД
Резюме
Outbox Pattern — это решение для надёжной доставки сообщений в распределённых системах, основанное на:
- Сохранении события в отдельной таблице OUTBOX вместе с основными данными (одна транзакция)
- Отдельном процессе, который читает из OUTBOX и отправляет события в очередь
- Гарантии, что события будут доставлены даже при сбоях
Это критически важный паттерн для надёжных микросервисных систем.