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

Для какой задачи может применяться 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));  // ЧТО ЕСЛИ ПАДЁТ?
        // Если здесь упадёт приложение:
        // - Заказ сохранён
        // - Сообщение НЕ отправлено
        // - Другие микросервисы не узнают о заказе!
    }
}

Проблемы:

  1. Потеря сообщения — если приложение падёт до отправки
  2. Несогласованность данных — БД и очередь в разных состояниях
  3. Дублирование — могут быть отправлены дубли при повторе

Решение: 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 — это решение для надёжной доставки сообщений в распределённых системах, основанное на:

  1. Сохранении события в отдельной таблице OUTBOX вместе с основными данными (одна транзакция)
  2. Отдельном процессе, который читает из OUTBOX и отправляет события в очередь
  3. Гарантии, что события будут доставлены даже при сбоях

Это критически важный паттерн для надёжных микросервисных систем.