
Работа с микросервисами достаточно сложная, как и с любой распределенной системой. В распределенной системе многое может пойти не так, и об этом даже написаны научные статьи. Если вы хотите углубиться в эту тему, советую почитать про заблуждения распределенных вычислений (fallacies of distributed computing). Уменьшение количества возможных точек отказа должно быть одной из целей инженера, который проектирует распределенную систему. В этом выпуске мы постараемся достичь именно этого, используя паттерн Outbox.
Как реализовать надежную связь между компонентами в распределенной системе?
Паттерн Outbox — это элегантное решение этой проблемы, позволяющее достичь транзакционных гарантий в рамках одного сервиса и обеспечить доставку сообщений во внешние системы по принципу "как минимум один раз" (at-least-once).
Давайте посмотрим, как паттерн Outbox решает эту задачу и как мы можем его реализовать.
Какую проблему решает паттерн Outbox?
Чтобы понять, какую проблему решает Outbox, нам сначала нужна сама проблема.
Вот пример потока регистрации пользователя. Здесь происходит несколько действий:
Сохранение пользователя в базу данных
Отправка приветственного письма пользователю
Публикация события
UserRegisteredEventв брокер
public async Task RegisterUserAsync(User user, CancellationToken token)
{
_userRepository.Insert(user);
await _unitOfWork.SaveChangesAsync(token);
await _emailService.SendWelcomeEmailAsync(user, token);
await _eventBus.PublishAsync(new UserRegisteredEvent(user.Id), token);
}
В "идеальном случае" все операции завершаются без ошибок, и все хорошо.
Но что произойдет, если одна из этих операций завершится некорректно?
База данных недоступна, и сохранение пользователя не удается
Почтовый сервис не работает, и отправка письма падает с ошибкой
Публикация события в брокер не проходит
Также представьте ситуацию, когда вам удалось сохранить пользователя в БД, отправить ему приветственное письмо, но не удалось опубликовать UserRegisteredEvent для уведомления других сервисов. Как вы будете восстанавливаться после такого сбоя?
Паттерн Outbox позволяет атомарно обновлять базу данных и отправлять сообщения в брокер сообщений.
Реализация паттерна Outbox
— добавить в базу данных таблицу, представляющую Outbox (Исходящие) сообщения. Мы можем назвать эту таблицу
OutboxMessages, и она предназначена для хранения всех сообщений, которые должны быть гарантированно доставлены потребителю. Теперь вместо прямых запросов к внешним сервисам мы просто сохраняем сообщение как новую строку в таблице Outbox. Сaми же сообщения, обычно, хранятся в базе данных в формате JSON.— внедрить фоновый процесс, который будет периодически опрашивать таблицу
OutboxMessages. Если рабочий процесс находит строку с необработанным сообщением, он публикует это сообщение и помечает его как отправленное. Если публикация сообщения по какой-то причине не удалась, рабочий процесс может повторить попытку при следующем запуске.
Обратите внимание, что благодаря повторным попыткам (retries) у вас теперь реализована доставка сообщений "как минимум один раз" (at-least-once). В случае успеха сообщение будет опубликовано ровно один раз, а в случае повторных попыток — более одного раза.
Мы можем переписать метод RegisterUserAsync из предыдущего примера, теперь он будет использовать паттерн Outbox:
public async Task RegisterUserAsync(User user, CancellationToken token)
{
_userRepository.Insert(user);
_outbox.Insert(new UserRegisteredEvent(user.Id));
await _unitOfWork.SaveChangesAsync(token);
}
Outbox является частью той же транзакции, что и наш Unit of Work, поэтому мы можем атомарно сохранить пользователя в базу данных и также сохранить OutboxMessage. Если сохранение в БД не удастся, вся транзакция откатится, и никакие сообщения не будут отправлены в шину сообщений.
А так как мы перенесли публикацию UserRegisteredEvent в рабочий процесс, нам нужно добавить обработчик, чтобы мы могли отправить приветственное письмо пользователю. Вот пример этого в классе SendWelcomeEmailHandler:
public class SendWelcomeEmailHandler : IHandle
{
private readonly IUserRepository _userRepository;
private readonly IEmailService _emailService;
public SendWelcomeEmailHandler(
IUserRepository userRepository,
IEmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
public async Task Handle(UserRegisteredEvent message)
{
var user = await _userRepository.GetByIdAsync(message.UserId);
await _emailService.SendWelcomeEmailAsync(user);
}
}
Архитектура проекта на основе Outbox

Если взглянуть на высокоуровневую архитектуру системы с внедренным Outbox, вы увидите таблицу Outbox в базе данных - изменилось то, что теперь вы сохраняете сообщения в таблицу Outbox в той же транзакции вместе с вашими энтити.
Дополнительные материалы
После прочтения этой статьи у вас должно сложиться хорошее понимание того, что такое паттерн Outbox и какие проблемы он решает. Если вам нужно реализовать надежный обмен сообщениями в распределенной системе, это отличное решение вашей проблемы.
Чего не хватает - так это более подробной информации о том, как реализовать шаблон "Outbox", поэтому вот несколько видеороликов, которые вы можете посмотреть: