понедельник, 24 августа 2009 г.

Транзакции и обеспечение правильного порядка асинхронного взаимодействия


Пару слов об истории проблемы. Я - разработчик бизнес-процессов в компании Naumen. Как я уже писал, основная активность бизнес-процесса (BPEL-процесса) - вызов неких сервисов (чаще всего - веб-сервисов). Фактически задача BPEL-процесса сводится к тому, чтобы обеспечить необходимый порядок вызова необходимых сервисов. Впрочем, BPEL взят лишь для примера, мысли, изложенные далее, характерны для взаимодействия любых систем. Так вот, при взаимодействии приложения и бизнес-процесса, а так же бизнес-процесса и приложения, иногда возникают интересные коллизии, вызванные неправильной организацией взаимодействия. Именно об этом я и хочу сегодня поговорить.


Существует два типа взаимодействия любых систем: синхронный и асинхронный. При синхронном взаимодействии выделяют некоторые фиксированные временные интервалы, через которые будет происходить взаимодействие. Например, мы знаем, что каждый веб-сервис отвечает нам через 100 мс, а промежуток между вызовами сервисов составляет 200 мс. Если мы это знаем, то нам очень просто писать процесс - достачно лишь обеспечить нужные задержки, не нужно реализовывать никаких лишних проверок и циклов.



При асинхронном взаимодействии картина сложнее. Мы не знаем через какие временные интервалы наши сервисы будут возвращать результат, мы не знаем когда мы будем вызывать сервисы. Главное на чем строится взаимодействие - механизм подтверждений. Т.е. мы посылаем запрос сервису и ждем, когда он нам ответит. Мы создаем задачи и подсчитываем подтверждения о завершении каждой. И т.д. С асинхронным взаимодействием мы сталкиваемся всегда, когда не знаем (часто и не можем знать) временных параметров компонентов системы.



Теперь вопрос, а причем здесь транзакции в БД? Как известо - транзакции спасают нас от грязного чтения - т.е. если в транзакции меняются данные, извне они не доступны, пока транзакция не будет завершена. А теперь рассмотрим такой момент: мы из процесса вызываем некий сервис, который пишет данные в БД (в транзакции А) и посылает уведомление процессу (реализуется асинхронное взаимодействие). Процесс, получив уведомление, вызывает другой метод этого же сервиса, который пытается прочитать недавно записаные данные из БД. И вот здесь возможны два варианта:

1. Транзакция А успела завершиться до вызова второго метода - будут прочитаны корректные данные



2. Транзакция А не успела завершиться до вызова второго метода - будут прочитаны некорректные данные, т.к. транзакция защищает нас от грязного чтения.



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

Сигнал уведомления должен отправляться BPEL-процессу после завершения транзакции. Казалось бы - очевидно, однако не совсем. Дело в том, что при разработке на Java во многих фреймворках программист не может сам вносить изменение в механизм транзакций. Например, транзакции реализуют с помощью Servlet-фильтров, т.е. для обработки HTTP-запроса создается одна транзакция и внутри нее мы уже вольны работать с БД. Другим примером может являться декларативное управление транзакциями с помощью SpringAOP, когда границы транзакции совпадают с границами метода (а границы метода - с границами вызываемого сервиса).



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

Что может здесь помочь? В частности - тот же SpringAOP, если вы разрабатываете приложение на Spring. С помощью AOP можно добиться вызова некоторого кода непосредственно после завершения нужных методов, а значит и транзакций.

В Naumen Kernel используется механизм событий. При завершении исполнения бизнес-действия (что совпадает с завершением транзакции) - генерируется соответствующее событие. Существует механизм обработки таких событий. Все действия по уведомлению внешних систем выносится в эти обработчики.

В системах, построенных на оборачивании всей обработки HTTP-запроса в транзакцию, даже не знаю, что может помочь. Скорее всего - использование тех же Servlet-фильтров, единственное - нужно будет обеспечить правильный порядок их вызова.

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

Вопросы, возражения, пожелания?

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

8 комментариев:

Никита Кокшаров комментирует...

Помним-помним мы этот Naumen Kernel... :)

Jungehexe комментирует...

ага, спасибо за практическую схему реализации.

Antón комментирует...

Очень недавно наткнулся на блог, оч. интересные статьи.

Как мне кажется, данный пример не совсем верен. Если система, где вертится процесс, а также транспорт, поддерживают distributed transactions, то транзакция A должна распространяться на процесс при возврате результата (это по сути синхронный callback-вызов с remote transaction). Это гарантирует, что пока сервис не сделает полный commit транзакции A, процесс дальше не сдвинется, пусть даже он и на другой системе.

Pavel Samolisov комментирует...

Система не поддерживает распределенные транзакции, именно в этом ее проблема и поэтому требуются такие решения.

Pavel Samolisov комментирует...

Собственно, вся статья - попытка объяснить такой use case в случае, если система не поддерживает распределенных транзакций.

Yacoder комментирует...

Поскольку я как раз занимаюсь базами данных, а именно согласованностью, то мне кажется чересчур кратким вот этот абзац:

Теперь вопрос, а причем здесь транзакции в БД? Как известо - транзакции спасают нас от грязного чтения - т.е. если в транзакции меняются данные, извне они не доступны, пока транзакция не будет завершена.

Возможно, описание это достаточно для целей данного поста, но теоретически оно не корректно. Чтобы оно было верным, от транзакционной системы (в данном случае СУБД) требуется поддержка определенного уровня изоляции транзакций -- это "I" из четверки ACID.

Во-первых, этот уровень может быть понижен настройками СУБД, в стандарте ANSI/ISO для SQL есть режим изоляции READ UNCOMITTED, который позволяет транзакции считывать любые данные, независимо от состояния блокировок.
Во-вторых, существуют (редкие на практике) режимы работы, когда от изоляции просто отказываются, поскольку она плохо работает для "долгоживущих" транзакций. Можно поискать по терминам long-lived transactions, sagas, nested transactions.

И в первом и во втором случае исходят из желания увеличить производительность СУБД (не всегда dirty read вреден для бизнес-задач).

А насчёт проблемы с асинхронными запросами, в распределенных системах еще иногда делают так: при первом обращении внешнего приложения ему дают какой-то хэндл или тикет или идентификатор сессии и затем он при обращениях к транзакционной системе этот тикет всегда предъявляет если хочет, чтобы все его действия были SERIALIZABLE. При этом сама система не обязательно, конечно, внутри такова, она просто блокирует вызовы извне если вот например происходит так, что предыдущая транзакция по данному тикету еще не завершилась...

Pavel Samolisov комментирует...

Спасибо за такой подробный и развернутый комментарий.

Мое описание грязного чтение естественно и не претендовало на полноту, но для целей поста его было действительно достаточно. Потому что при настройках СУБД по умолчанию (по крайней мере в PostgreSQL) чтение именно атомарно.

Про механизм тикетов не знал. Действительно интересно, надо будет разобраться.

Yacoder комментирует...

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

Про это больше говорят в контексте не изоляции, а согласованности данных в распределенных системах... Интересная статья есть тут, можно смотреть раздел Client side consistency:

http://www.allthingsdistributed.com/2007/12/eventually_consistent.html

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!