В шине сообщений в сервисной архитектуре, сообщение не должно быть командой.

Хотя команда — на первый взгляд более простой и очевидный путь реализации, но он приводит к большим сложностям.

Пример общения двух сервисов

Представим, что у нас есть два сервиса. Один сервис отвечает за регистрацию, другой за бонусную систему для пользователей.

Сервис регистрации и сервис бонусов

У каждого сервиса, как положено, есть собственная БД, это независимые процессы которые работают параллельно.

Они общаются друг с другом через шину сообщений. Сообщения отправленные одним сервисом, будут получены и обработаны другим сервисом.

Шина сообщений

Бонусы при регистрации

Представим, что есть бизнес-правило: при регистрации нового пользователя, начислять ему 50 бонусов на счёт.

При этом регистрация происходит в сервисе регистраций, а начисление бонусов происходит в сервисе бонусов.

Как это сделать?

Команды

Помещаем в сервис регистраций код: при регистрации отправить сообщение в сервис бонусов, чтобы тот начислил 50 бонусов.

Это будет наша команда: “Начислить 50 бонусов пользователю X”.

Сообщение в виде команды

Шину сообщений здесь и далее не показываю чтобы упростить рисунок, но она подразумевается.

Сообщения ходят, бонусы начисляются, всё хорошо.

Или нет?

Какие недостатки есть у этого способа?

Потеря сообщений: команды

Что делать, если сообщение было утеряно? Что делать, если мы не можем точно сказать, было оно утеряно или доставлено?

Если сообщение является уведомлением о произошедшем событии, то мы можем просто отправить его повторно. Приëмник должен быть спроектирован таким образом, чтобы повторная отправка сообщения ничего не сломала.

Если сообщение было командой, то повторная отправка может привести к ещë большей рассинхронизации и трудноотслеживаемым багам.

В нашем примере, команда отправленная дважды начислит вдвое больше бонусов, чем нужно. Пользователь зарегистрировался один раз, а начисление бонусов произойдёт два раза.

События

Тот же пример, но связываем сервисы с помощью событий, а не команд.

Сообщение о событии является простым уведомлением о том, что что-то уже произошло. В одном из сервисов произошли изменения, и он уведомляет об этом остальные сервисы.

В этом ключевое различие между командой и событием. Команда: “сделай это”. Событие: “произошло такое-то событие”.

Наше событие: “Зарегистрирован пользователь X”.

Сообщение в виде события

В этом случае, сервис регистраций не указывает, что нужно сделать, а только говорит о том, что произошло: пользователь был зарегистрирован.

Реагируя на это событие, сервис бонусов начисляет 50 бонусов на счёт пользователя.

Потеря сообщений: события

Допустим, некоторые из сообщений не дошли, и мы точно не уверены, какие сообщения доставлены и обработаны, какие нет.

Отправим их повторно, чтобы точно обработались.

Что будет при повторной отправке, если сообщения являются событиями?

Повторная отправка сообщения о событии регистрации может привести к тому, что бонусы начислены повторно.

Мы опять влипли в ситуацию, когда один раз зарегистрировавшийся пользователь получает бонусы дважды.

Есть ли отличие? Есть.

В случае обработки событий, а не команд, мы можем спроектировать сервис бонусов так, чтобы обработка сообщений была “идемпотентной”.

Простыми словами, мы проектируем сервис так, чтобы дважды отправленное событие обработалось только один раз.

Как это реализовать на практике?

Мы можем создать в сервисе бонусов таблицу “пользователи, получившие бонус при регистрации”.

При обработке события о регистрации, мы проверяем, есть ли ID пользователя в этой таблице.

Если ID есть в таблице, то игнорируем событие. Если ID нет в таблице, то добавляем ID в таблицу и начисляем бонусы.

Таким образом, каждое событие будет обработано только один раз и система станет устойчивой к потере событий.

События vs Команды

Сервисная архитектура, в которой в качестве сообщений используются события, а не команды, более устойчива и надёжна.

Зная о причинах этой устойчивости, вы легко отличите один способ от другого.