Доброе утро, в нашем проекте у нас есть REST-контроллер, который выглядит так (это всего лишь псевдокод, а не точный случай, здесь я привожу простой пример, показывающий общую концепцию):
@Transactional
public ResponesEntity<Void> compisiteCreate() {
customerService.createCustomer()
productService.createProduct()
orderService.createOrder()
}
Фабричный метод каждого из сервисов (клиент, продукт и заказ) — это отдельный вариант использования. Он сохраняет запись в базе данных, и в случае успешного сохранения отправляется соответствующее событие. Кроме того, каждый из методов помечен @Transactional
, так как каждый из них вызывается выделенной конечной точкой. Таким образом, мы гарантируем, что в случае возникновения исключения ни запись не будет сохранена в базе данных, ни событие не будет отправлено для определенного сценария.
Теперь требование состоит в том, чтобы иметь конечную точку compositeCreate
(представленную выше), которая должна быть атомарной, то есть, если createOrder
терпит неудачу, и createCustomer
, и createProduct
откатываются. Проблема в событиях. Отправленное событие является синхронным и не может быть отменено (запустить и забыть). Следовательно, если третий метод не работает, но первые два в порядке, события, связанные с первым методом, уже отправлены, но запись не будет сохранена в БД.
Моя первая мысль здесь заключалась в том, чтобы обернуть эту логику в какой-то «сеанс диспетчера событий». Внутри этого сеанса события не отправляются синхронно, а ставятся в очередь до окончания сеанса. Затем в конце логики контроллера, когда код внутри проходит без исключений, мы закрываем сеанс, который будет отправлять все события в очереди. Псевдокод может выглядеть так:
@Transactional
public ResponesEntity<Void> compisiteCreate() {
eventDispatcher.startSession()
customerService.createCustomer()
productService.createProduct()
orderService.createOrder()
eventDispatcher.flushSession()
}
Сеанс диспетчера событий будет связан с потоком (например, с использованием ThreadLocal), чтобы обеспечить один сеанс для каждого запроса. Если сессия не запущена, события отправляются синхронно, без каких-либо изменений.
Ожидаемый результат: Либо все сохраняется и все события отправляются, либо ничего не сохраняется и никакие события не отправляются.
Вопрос: Имеет ли эта идея смысл? Может быть, кто-то может увидеть уже какие-то недостатки? А может быть, эта идея совершенно неверна? У кого-нибудь есть другие идеи, как решить эту проблему?
How to ensure composite commands including persisting to db and dispatching events are atomic?
«Исходящие шаблоны»: запись событий в базу данных как часть транзакции; беспокойтесь о публикации событий отдельно.
См. Надежный обмен сообщениями без распределенных транзакций, автор Уди Дахан.
Either everything is persisted and all events are dispatched or nothing is persisted and no events are dispatched.
Поскольку у нас есть физические ограничения (например, конечная скорость света), все или ничего не является гарантией того, что мы можем это сделать. Мы можем управлять более слабыми утверждениями, такими как «не более одного раза» или «хотя бы один раз».
«По крайней мере один раз» — более слабое утверждение, которое на самом деле полезно, но оно означает, что подписчики должны уметь распознавать повторяющиеся сообщения — другими словами, вам нужна идемпотентная обработка сообщений.
См. также де Граав 2010: Никому не нужен надежный обмен сообщениями