"Как гарантировать доставку асинхронных сообщений на стороне отправителя?" - Этот вопрос подводит нас, пожалуй, к самому популярному вопросу на собеседовании в контексте распределенных взаимодействий. А именно, об использовании архитектурного паттерна Transactional Outbox.
Когда приложение публикует сообщения в брокер, возникает проблема консистентности данных.
👤 Пример: Создание пользователя
Task<long> CreateUser(string email);
Метод CreateUser создает пользователя по email и отправляет сообщение UserCreated с информацией о пользователе в брокер для обработки другими сервисами. Например, для отправки email с подтверждением регистрации.
🔄 Стандартная реализация
Реализацию метода можно описать следующими шагами:
public Task<long> CreateUser(string email)
{
// 1. Создать пользователя в БД
// 2. Отправить сообщение UserCreated в брокер сообщений
}⚠️ Проблемы стандартного подхода
В этом сценарии может возникнуть ситуация, при которой данные о пользователе сохранятся в БД на этапе 1, а сообщение не отправится в брокер на этапе 2 из-за проблем с сетью или недоступности брокера. Сервис отправки email не получит сообщения и не отправит email пользователю, что приведет к неконсистентности системы.
Можно предположить, что изменение порядка шагов 1 и 2 поможет, но в этом случае возникает обратная ситуация. Сообщение отправится в брокер, а данные о пользователе не сохранятся в БД.
💡 Решение: Паттерн Transactional Outbox
Для того чтобы гарантировать атомарное сохранение данных и отправку сообщения применяют паттерн Transactional Outbox.
📦 Этап 1: Сохранение в Outbox
Вместо того, чтобы сразу отправлять сообщение в брокер. Приложение будет сохранять сообщение в специальную таблицу outbox в БД в одной транзакции с сохранением данных о пользователе. Таким образом в случае возникновения проблем при обработке запроса CreateUser данные о пользователе и сообщение сохранятся в БД вместе, или не сохранятся вовсе.
public Task<long> CreateUser(string email)
{
// 1. Открыть транзакцию
// 2. Создать пользователя в БД
// 3. Сохранить сообщение UserCreated в БД
// 4. Подтвердить транзакцию
}📤 Этап 2: Обработка Outbox
Далее задача по расписанию в транзакции читает несколько сообщений из таблицы outbox, отправляет их в брокер сообщений и удаляет или помечает обработанными. Это может быть фоновая задача в приложении с API или отдельно стоящий сервис. После отправки вычитанных сообщений в брокер и удаления, выполняется подтверждение транзакции.
Task ProcessOutbox()
{
// 1. Открыть транзакцию к БД
// 2. Прочитать 10 сообщений из таблицы outbox
// 3. Отправить сообщения в брокер
// 4. Удалить сообщения
// 5. Подтвердить транзакцию
}🔧 Как это работает
Таким образом создание пользователя с отправкой сообщений разбивается на два этапа:
- Атомарное сохранение пользователя и сообщения в БД;
- Отправка сообщений из таблицы
outboxв брокер.
⚡️ Семантика At Least Once
На этапе 2 возможны ситуации, когда после отправки части прочитанных в транзакции сообщений, брокер оказывается недоступным. В этом случае транзакция отменяется и в следующей сообщения могут быть отправлены повторно. Поэтому Transactional Outbox гарантирует доставку сообщения хотя бы один раз. Это семантика - At Least Once.
✅ Итоги
В совокупности использование Transactional Outbox гарантирует консистентное состояние системы на стороне отправки сообщений. Либо бизнес-операция выполняется целиком, либо не выполняется вовсе.
Ты узнал о новом архитектурном паттерне, который поможет лучше отвечать на собеседованиях.
#техничка #backend