Отправка событий в конце транзакции - PullRequest
11 голосов
/ 12 августа 2011

У меня есть интерфейс для объекта Service, который выглядит примерно так (упрощенно для краткости):

public interface ItemService {

  public Item getItemById(String itemId, int version);

  public void create(Item item, User user);

  public void update(Item item, User user);

  public void delete(Item item, User user);

}

ItemService единственная реализация и подключен как bean-компонент Spring.Он используется частью пользовательского интерфейса нашего проекта и кодом, который обрабатывает запросы Ajax, для создания и изменения Item объектов в нашем хранилище данных.

Под капотом каждый метод отправляет серию событийкогда это будет вызвано.События принимаются другими модулями для выполнения таких задач, как обновление индексов Lucene или для отправки сообщений администраторам, чтобы они знали, что что-то изменилось.Каждый вызов метода представляет собой отдельную транзакцию в Spring (с использованием org.springframework.orm.hibernate3.HibernateTransactionManager и org.springframework.transaction.interceptor.TransactionProxyFactoryBean).

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

*Begin transaction*
Get Items created by User Bill using ItemService
 for each Item in Items 
   Update field on Item
   Link Item to User Bill with LinkService 
   Update Item using ItemService
*Finish transaction* 

Мы сделали это, создав другую Службу, которая позволяет вам составлять вызовы от Служб в одном методе в родительской службе.Давайте назовем это ComposingService.ComposingService, как и все остальные, также управляется Spring, и, поскольку транзакции реентерабельны, все это должно работать ..

Однако существует проблема: если какая-либо из этих операций внутри транзакции завершится неудачноВ результате отката транзакции мы не хотим отправлять какие-либо события .

В существующем случае, если транзакция завершится неудачей на полпути, половина Событий будет отправлена ​​к ItemService до того, как транзакция откатится, а это означает, что некоторые модули получат кучу Событий за вещи, которые не произошли,

Мы пытаемся найти способ исправить это, но мы не могли придумать ничего элегантного.Лучшее, что мы придумали до сих пор, это что-то вроде этого (и это уродливо):

public interface ItemService {

  public Item getItemById(String itemId, int version);

  public void create(Item item, User user, List<Event> events);

  public void update(Item item, User user, List<Event> events);

  public void delete(Item item, User user, List<Event> events);

}

В этом модифицированном ItemService вместо отправляемых немедленно событий они добавляются в список.События передаются в качестве аргумента.Список поддерживается ComposingService, а События отправляются ComposingService после успешного завершения всех вызовов на ItemService и других служб.

Очевидно, проблема в том, что мы изменили контракт на ItemService безобразно.Вызовам классов, даже если они являются службами, не нужно беспокоиться об управлении событиями.Но я не смог придумать способ обойти это, отсюда и этот вопрос.

Это похоже на проблему, которая, возможно, была решена раньше.У кого-нибудь была проблема, которая выглядит похожей, и если да, то как вы ее решили?

Ответы [ 3 ]

22 голосов
/ 12 августа 2011

Подводя итог вашего вопроса: Вы ищете безопасный для транзакций способ отправки сообщений.

Вариант 1: JMS

Транзакционно-безопасный обмен сообщениями - это именно то, для чего нужен JMS.Существует также хорошая интеграция с JMS в Spring, см. Главу JMS в документации Spring.

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

Разница с вашей текущей настройкой заключается в том, что эти события будут обрабатываться асинхронно: ваш сервис вернется до того, как эти события будут обработаны.(JMS позаботится о том, чтобы они были обработаны в конечном итоге, его можно настроить на многократные попытки и способы устранения ошибок, ...).В зависимости от ваших потребностей, это может быть хорошо или плохо.

Вариант 2: синхронизация транзакций

В качестве альтернативы, если JMS слишком тяжелый для вашего случаяВы можете использовать синхронизацию транзакций: при отправке события вместо прямой отправки используйте Spring 101 * 1019 и отправьте сообщение в afterCommit() вашего TransactionSynchronization.Вы можете либо добавить новую синхронизацию для каждого отправляемого события, либо добавить одну синхронизацию и отслеживать, какие события следует отправлять, привязав объект, содержащий этот список, к транзакции, используя TransactionSynchronizationManager.bindResource.

. Я бы посоветовалпротив попыток использовать свой собственный ThreadLocal для этого, потому что в некоторых случаях это может пойти не так;например, если внутри вашей транзакции вы начнете новую транзакцию (RequiresNew).

Различия с текущей настройкой:

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

В качестве альтернативы, вы можете использовать beforeCommit вместо afterCommit, но тогда ваши события будут обработаны (письма отправлены, ...), даже если впоследствии произойдет сбой фактической фиксации в базе данных.

Это менее надежно ( меньше транзакций ), чем использованиеJMS, но легче и проще в настройке, и обычно достаточно хорош .

1 голос
/ 12 августа 2011

Как упомянул @Wouter, одной из альтернатив является использование методов асинхронного обмена сообщениями.Я могу подумать о 2 других подходах:

  1. события, сгенерированные ItemService, сохраняются в таблице базы данных после фиксации (поэтому при откате они не доступны).Фоновое (асинхронное) задание анализирует события и вызывает соответствующие службы.Вы могли бы назвать это «самодельным JMS».Возможная альтернатива, если инфраструктура обмена сообщениями недоступна.
  2. Используйте ThreadLocal , чтобы временно поставить в очередь все события и после того, как составные транзакции будут зафиксированы, запустить события.Это не является постоянным и может завершиться ошибкой, например, после коммита и до того, как все события будут сняты.Я не фанат ThreadLocal , но это лучше, чем путать бизнес-интерфейсы (ItemService) с несвязанными параметрами.См. Это для реализации , связанной с пружиной .

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

1 голос
/ 12 августа 2011

В отличие от Wouter Coekaerts, я понимаю, что вы запрашиваете способ отправки уведомлений, которые будут отправляться только получателю, если транзакция, в которой они были созданы, была успешной.- Итак, вы ищете что-то похожее на транзакционный механизм CDI-событий.

Моя идея решить эту проблему так:

  • sendсобытия и «сохранить» их в списке в вашем механизме обработки событий, но не пересылать события получателям.(Я думаю, это легко реализовать)
  • пересылать события из списка получателям, если транзакция прошла успешно
  • удалить события из списка, если транзакция имеет откат

Чтобы переслать или удалить события, я сначала взгляну на механизм транзакций пружины.Если нет возможности расширить его, вы можете написать аспект AOP, который будет пересылать события, если метод, помеченный @Transactional, оставлял без исключения (Runtime).Но если аннотированный метод @Transactional выходил с исключением (время выполнения), удалите события из списка.

...