Предположим, есть веб-сервис, который принимает POST-запрос на регистрацию пользователя. Первоначальная реализация (намеренно очень упрощенная):
[HttpPost]
public ActionResult Post([FromBody]CustomerDTO value)
{
try
{
_customerRepository.Create(value);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
Я захотел распределить нагрузку между несколькими серверами, и вместо того, чтобы дожидаться сохранения в репозитории, я хочу послать команду "Зарегистрировать пользователя" в шину. Она попадет в очередь, например, RabbitMQ. Далее, эту команду обработает один из серверов, который подписан на выполнение этой команды. Что-то вроде того:
[HttpPost]
public ActionResult Post([FromBody]CustomerDTO value)
{
try
{
RegisterCustomerCommand command = MapFromDto<RegisterCustomerCommand>(value);
_bus.Send(command)
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
...
public class RegisterCustomerCommandHandler: ICommandHandler<RegisterCustomerCommand>
{
private ICustomerRepository _customerRepository;
public RegisterCustomerCommandHandler(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public void Handle(RegisterCustomerCommand command)
{
_customerRepository.Create(GetDTOFromCommand(command));
}
}
Однако, допустим, согласно логике приложения мы не можем зарегистрировать двух пользователей с одинаковым E-mail. В случае реализации сервисной шины мы имеем опасность в одно и то же время получить запросы на регистрацию пользоватаеля с одинаковыми E-mail. Клиенту WebApi вернется код 200, что значит что регистрация прошла успешно. Однако одна из команд неминуемо упадёт (т.к. попытается зарегистрировать пользователя с E-mail, который пару мгновений назад уже был зарегистирован), но клиент уже об этом не узнает.
Предварительная валидация (не зарегистрирован ли уже пользователь с таким Email?) тут тоже не поможет - в момент валидации он может быть не зарегистрирован, а в момент выполнения команды - уже зарегистрирован.
Что вы думаете об этой ситуации? Как вы поступаете в таком случае?
Мои мысли по этому вопросу: такая реализация отлично подойдет, например, в случае, когда надо послать пользователю электронное письмо с ссылкой подтверждения Email. Зарегистрировали пользователя --> создали событие "CustomerRegistredEvent", послали в очередь --> это событие из очереди через n секунд заберет обработчик и пошлёт письмо пользователю. Однако в "чувствительных" случаях, таких как тот, что описан выше (регистрация пользователя) необходимо дождаться выполнения всей команды. Возможно, тут не надо применять Команду и Шину.
Возможно в мире распределенных систем данная задача решается иначе, но если отбросить специфику конкретной реализации, то выглядит, как самая обыкновенная асинхронная задача.
Ты регистрируешь команду в очереди и отдаёшь пользователю её идентификатор и примерное время, за которое она будет выполнена. Спустя это время пользователь спрашивает о текущем состоянии задачи. Получает либо результат выполнения, либо новый эстимейт по времени. И так пока сервер не скажет клиенту, что задача завершена с определенным результатом, либо пока клиенту не надоест ждать (и в этом случае стоит хотя бы попытаться отправить запрос на отмену), либо пока задача каким-нибудь нехорошим образом не исчезнет из истории (н.п. если сервер перезагрузили). Эти ситуации клиент обрабатывает самостоятельно и либо повторно пытается выполнить задачу, либо информирует пользователя о том, что что-то пошло не так.
Продвижение своими сайтами как стратегия роста и независимости