Когда отправлять событие в очередь: перед транзакцией или после?

Ответ

Событие следует отправлять в очередь после успешного завершения транзакции в базе данных. Это гарантирует консистентность: если транзакция откатится, событие о её успехе не будет отправлено.

Проблемный подход (отправка внутри транзакции):

using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        var order = new Order { /* ... */ };
        context.Orders.Add(order);
        context.SaveChanges(); // Сохраняем в БД

        // ОПАСНО: событие отправляется в очередь (например, RabbitMQ)
        _eventBus.Publish(new OrderCreatedEvent(order.Id));
        // Если здесь произойдет сбой, транзакция откатится,
        // но событие уже ушло и не может быть отозвано.

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

Корректный подход (отправка после транзакции):

using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        var order = new Order { /* ... */ };
        context.Orders.Add(order);
        context.SaveChanges();
        transaction.Commit(); // Транзакция успешно завершена
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}
// Только теперь, гарантированно после фиксации транзакции, отправляем событие.
_eventBus.Publish(new OrderCreatedEvent(order.Id));

Исключение: Если брокер очередей (например, база данных, используемая как очередь) поддерживает распределённые транзакции (XA), то отправку можно выполнять внутри общей транзакции. Однако этот паттерн сложен и менее распространён в микросервисных архитектурах.

Ответ 18+ 🔞

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

Смотри, есть у тебя заказ в базе. Сохранил — и сразу в очередь событие пихнул, типа «ой, всё готово, ребята, обрабатывайте». А потом бац — и транзакция откатилась по какой-то хуевой причине. И что мы имеем? В базе заказа нет, нихуя, пустота. А в очереди уже болтается событие, которое всем кричит: «Заказ создан, иди сюда!». И понеслась: другие сервисы начинают этот несуществующий заказ искать, обновлять, хуярить по нему отчёты — пиздец полный, бардак на овердохуища. Консистентность, блядь, накрылась медным тазом. Терпения ноль ебать с такими подходами.

Вот смотри на этот кусок кода — тут прямо хитрая жопа притаилась:

using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        var order = new Order { /* ... */ };
        context.Orders.Add(order);
        context.SaveChanges(); // Сохраняем в БД

        // ОПАСНО: событие отправляется в очередь (например, RabbitMQ)
        _eventBus.Publish(new OrderCreatedEvent(order.Id));
        // Если здесь произойдет сбой, транзакция откатится,
        // но событие уже ушло и не может быть отозвано.

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

Видишь подвох? Сохранили заказ (ну, в контексте, не в базе ещё), а потом — ёперный театр — сразу же выстрелили событием в шину. А что, если между SaveChanges() и transaction.Commit() что-то пойдёт по пизде? Например, соседний запрос накроется, или констрейнт какой ебучий сработает при коммите? Транзакция откатится, заказ испарится, а событие-то уже улетело, как хуй с горы, и его не догонишь. Получается, мы наёбываем всю систему, отправляя фейковые уведомления. Доверия к такому коду — ноль ебать.

А теперь правильный путь, без этих детских болезней:

using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        var order = new Order { /* ... */ };
        context.Orders.Add(order);
        context.SaveChanges();
        transaction.Commit(); // Вот теперь всё, точка. Транзакция в базе закрыта.
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}
// Абсолютная уверенность, что в базе заказ есть. ТОЛЬКО теперь можно кричать об этом на весь мир.
_eventBus.Publish(new OrderCreatedEvent(order.Id));

Вот это — красота. Сначала твёрдо фиксируем факт в основном хранилище. Всё, база рулит. А уже потом, как верный оруженосец, бежим разносить новости. Если отправка события сломается — это, конечно, тоже пиздец, но уже другой. Хотя бы не будет ситуации «в базе нет, а в очереди есть». Можно событие переотправить, логи обработать — жить можно.

Но! Есть же, блядь, умники, которые скажут: «А вот если у нас брокер поддерживает XA, распределённые транзакции, то можно и внутри делать!». Ну да, можно. Теоретически. Это как управлять реактивным самолётом, имея права на трактор. Паттерн тот ещё, сложный, доверия ебать ноль к его стабильности в микросервисах, и обычно от него все шарахаются как чёрт от ладана. Если, конечно, ты не любитель приключений на свою жопу.

Короче, правило простое, чувак: сначала твёрдо положи в базу, потом уже голоси в очередь. Не пытайся быть умнее всех и не создавай проблем на ровном месте. А то будет тебе не хиросима, а настоящий нигерсраки в продакшене.