
Работа с микросервисами достаточно сложная, как и с любой распределенной системой. В распределенной системе многое может пойти не так, и об этом даже написаны научные статьи. Если вы хотите углубиться в эту тему, советую почитать про заблуждения распределенных вычислений (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", поэтому вот несколько видеороликов, которые вы можете посмотреть:
