Пару слов об истории проблемы. Я - разработчик бизнес-процессов в компании Naumen. Как я уже писал, основная активность бизнес-процесса (BPEL-процесса) - вызов неких сервисов (чаще всего - веб-сервисов). Фактически задача BPEL-процесса сводится к тому, чтобы обеспечить необходимый порядок вызова необходимых сервисов. Впрочем, BPEL взят лишь для примера, мысли, изложенные далее, характерны для взаимодействия любых систем. Так вот, при взаимодействии приложения и бизнес-процесса, а так же бизнес-процесса и приложения, иногда возникают интересные коллизии, вызванные неправильной организацией взаимодействия. Именно об этом я и хочу сегодня поговорить.
Существует два типа взаимодействия любых систем: синхронный и асинхронный. При синхронном взаимодействии выделяют некоторые фиксированные временные интервалы, через которые будет происходить взаимодействие. Например, мы знаем, что каждый веб-сервис отвечает нам через 100 мс, а промежуток между вызовами сервисов составляет 200 мс. Если мы это знаем, то нам очень просто писать процесс - достачно лишь обеспечить нужные задержки, не нужно реализовывать никаких лишних проверок и циклов.
При асинхронном взаимодействии картина сложнее. Мы не знаем через какие временные интервалы наши сервисы будут возвращать результат, мы не знаем когда мы будем вызывать сервисы. Главное на чем строится взаимодействие - механизм подтверждений. Т.е. мы посылаем запрос сервису и ждем, когда он нам ответит. Мы создаем задачи и подсчитываем подтверждения о завершении каждой. И т.д. С асинхронным взаимодействием мы сталкиваемся всегда, когда не знаем (часто и не можем знать) временных параметров компонентов системы.
Теперь вопрос, а причем здесь транзакции в БД? Как известо - транзакции спасают нас от грязного чтения - т.е. если в транзакции меняются данные, извне они не доступны, пока транзакция не будет завершена. А теперь рассмотрим такой момент: мы из процесса вызываем некий сервис, который пишет данные в БД (в транзакции А) и посылает уведомление процессу (реализуется асинхронное взаимодействие). Процесс, получив уведомление, вызывает другой метод этого же сервиса, который пытается прочитать недавно записаные данные из БД. И вот здесь возможны два варианта:
1. Транзакция А успела завершиться до вызова второго метода - будут прочитаны корректные данные
2. Транзакция А не успела завершиться до вызова второго метода - будут прочитаны некорректные данные, т.к. транзакция защищает нас от грязного чтения.
Почему такое возможно? Потому что мы делаем грубейшую ошибку - обеспечиваем асинхронное взаимодействие, выбрав неправильное время для сигнала уведомления.
Сигнал уведомления должен отправляться BPEL-процессу после завершения транзакции. Казалось бы - очевидно, однако не совсем. Дело в том, что при разработке на Java во многих фреймворках программист не может сам вносить изменение в механизм транзакций. Например, транзакции реализуют с помощью Servlet-фильтров, т.е. для обработки HTTP-запроса создается одна транзакция и внутри нее мы уже вольны работать с БД. Другим примером может являться декларативное управление транзакциями с помощью SpringAOP, когда границы транзакции совпадают с границами метода (а границы метода - с границами вызываемого сервиса).
Поэтому так легко вызвать отправку уведомления внутри этого же метода, забывая о том, что транзакция будет завершена только после отправки уведомления и, как назло, может начаться сборка мусора, а BPEL-машина работает на другой JDK и успеет послать нам новый запрос, который придет к нам аккурат перед коммитом нашей транзакции.
Что может здесь помочь? В частности - тот же SpringAOP, если вы разрабатываете приложение на Spring. С помощью AOP можно добиться вызова некоторого кода непосредственно после завершения нужных методов, а значит и транзакций.
В Naumen Kernel используется механизм событий. При завершении исполнения бизнес-действия (что совпадает с завершением транзакции) - генерируется соответствующее событие. Существует механизм обработки таких событий. Все действия по уведомлению внешних систем выносится в эти обработчики.
В системах, построенных на оборачивании всей обработки HTTP-запроса в транзакцию, даже не знаю, что может помочь. Скорее всего - использование тех же Servlet-фильтров, единственное - нужно будет обеспечить правильный порядок их вызова.
Вывод: при реализации асинхронного взаимодействия самое главное, что нужно решить - выбрать правильные моменты для отправки подтверждений и обеспечить стабильность их возникновения. Стабильность формирования подтверждений может быть обеспечена путем синхронизации процесса формирования некоторым сигналом - например, сигналом о завершении транзакции.
Вопросы, возражения, пожелания?
Понравилось сообщение - подпишитесь на блог или читайте меня в twitter
6 комментариев:
Помним-помним мы этот Naumen Kernel... :)
ага, спасибо за практическую схему реализации.
Очень недавно наткнулся на блог, оч. интересные статьи.
Как мне кажется, данный пример не совсем верен. Если система, где вертится процесс, а также транспорт, поддерживают distributed transactions, то транзакция A должна распространяться на процесс при возврате результата (это по сути синхронный callback-вызов с remote transaction). Это гарантирует, что пока сервис не сделает полный commit транзакции A, процесс дальше не сдвинется, пусть даже он и на другой системе.
Система не поддерживает распределенные транзакции, именно в этом ее проблема и поэтому требуются такие решения.
Собственно, вся статья - попытка объяснить такой use case в случае, если система не поддерживает распределенных транзакций.
Спасибо за такой подробный и развернутый комментарий.
Мое описание грязного чтение естественно и не претендовало на полноту, но для целей поста его было действительно достаточно. Потому что при настройках СУБД по умолчанию (по крайней мере в PostgreSQL) чтение именно атомарно.
Про механизм тикетов не знал. Действительно интересно, надо будет разобраться.
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!