Статьи

Глобальная согласованность данных в микросервисных архитектурах

Я опубликовал общий адаптер ресурсов JCA на  Github,  доступный от Maven ( ch.maxant: genericconnector-rar ) с  лицензией Apache 2.0 . Это позволяет связывать такие вещи, как веб-службы REST и SOAP, в транзакции JTA, которые находятся под управлением серверов приложений Java EE. Это позволяет очень легко создавать системы, которые гарантируют непротиворечивость данных, используя как можно меньше кода котельной пластины. Обязательно прочитайте  FAQ

Представьте себе следующий сценарий …

Функциональные требования

  • … много страниц требований к сну …
  • FR-4053: Когда пользователь нажимает кнопку «купить», система должна забронировать билеты, совершить подтвержденную платежную транзакцию с покупателем и отправить клиенту письмо, включая распечатанные билеты и квитанцию.
  • … много других требований …

Избранные нефункциональные требования

  • NFR-08: Билеты должны быть забронированы с использованием NBS (корпорация «Nouvelle Booking System»), веб-службы HTTP SOAP, развернутой в интрасети.
  • NFR-19: Управление выводом (печать и отправка писем) должно осуществляться с использованием COMS, в интрасети также развернута служба JSON / REST.
  • NFR-22: Оплата должна производиться с использованием системы MMF (Make Money Fast) нашего Партнера, развернутой в Интернете и подключенной к VPN.
  • NFR-34: система должна записывать записи заказа на продажу в свою собственную базу данных Oracle.
  • NFR-45: данные, относящиеся к одному заказу на продажу, должны быть единообразными для всей системы, NBS, COMS и MMF.
  • NFR-84: Система должна быть реализована с использованием Java EE 6, чтобы ее можно было развернуть в нашей кластерной среде сервера приложений.
  • NFR-99: благодаря проекту MIGRATION’16 система должна быть построена так, чтобы она была переносимой на другой сервер приложений.

Анализ

NFR-45 это интересно. Мы должны гарантировать, что данные в нескольких системах остаются согласованными, т.е. даже во время сбоев программного / аппаратного обеспечения. Все же NFR-08, NFR-19, NFR-22 и NFR-34 усложняют ситуацию.  SOAP и REST не поддерживают транзакции!  — Нет, это не совсем так. Мы могли бы очень легко использовать что-то вроде менеджера транзакций Arjuna от JBoss, который поддерживает  WS-AT . Посмотрите, например,  этот проект  (или его источник на Github)  или  этот пример JBoss  или действительно  этот пример Metro . Однако есть несколько проблем с этими решениями: NFR-99 (используемые API не являются переносимыми); NFR-19 (REST не поддерживает WS-AT, хотя что- то находится  в процессе разработки в JBoss); и тот факт, что веб-сервисы, которые мы интегрируем, могут даже не поддерживать WS-AT. В прошлом я интегрировал много внутренних и внешних веб-сервисов, но никогда не сталкивался с тем, который поддерживает WS-AT. 

На протяжении многих лет я работал над проектами, которые предъявляли аналогичные требования, но давали разные решения. Я видел и слышал о компаниях, которые в итоге эффективно создают собственных менеджеров транзакций, которые связывают веб-сервисы с транзакциями. Я также сталкивался с компаниями, которые не беспокоятся о последовательности и игнорируют NFR-45. Мне нравится идея согласованности, но мне не нравится идея о том, что бизнес-проект, создающий среду, которая отслеживает состояние транзакций и вручную фиксирует или откатывает их, пытаясь синхронизироваться с диспетчером транзакций Java EE. Поэтому несколько лет назад у меня была идея, как выполнить все эти требования, и при этом избежать такого сложного решения, которое было похоже на создание менеджера транзакций.NFR-84 почти приходит на помощь, потому что серверы приложений Java EE поддерживают распределенные транзакции. Я написал «почти», потому что не хватает какой-то формы адаптера для привязки нестандартных ресурсов, таких как веб-сервисы, к таким транзакциям. Но спецификации Java EE также содержат JSR-112 , спецификация JCA для создания адаптеров ресурсов, которые могут быть связаны в распределенные транзакции. Моя идея состояла в том, чтобы создать универсальный адаптер ресурсов, который можно было бы использовать для привязки веб-сервисов и других вещей к транзакции под управлением сервера приложений, с минимально необходимой конфигурацией и с таким простым API, как я мог спроектировать.

Фон для распределенных транзакций

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

mysql> XA START 'someTxId';

mysql> insert into person values (null, 'ant');

mysql> XA END 'someTxId';

mysql> XA PREPARE 'someTxId';

mysql> XA COMMIT 'someTxId';

mysql> select * from person;
+-----+-------------------------------+
| id  | name                          |
+-----+-------------------------------+
| 771 | ant                           |
+-----+-------------------------------+

Листинг 1: транзакция XA в SQL

Ветвь глобальной транзакции запускается в базе данных (диспетчере ресурсов) в строке 1. Может использоваться любой произвольный идентификатор транзакции, и обычно глобальный менеджер транзакций на сервере приложений генерирует этот идентификатор. В строке 3 выполняется «бизнес-код», т. Е. Все утверждения, касающиеся того, почему мы используем базу данных, т.е. для вставки данных и выполнения запросов. Как только все эти бизнес-процессы завершены, и между строками 1 и 5 вы можете вызывать другие удаленные ресурсы, транзакция завершается с использованием строки 5. Обратите внимание, что транзакция еще не завершена, она просто переходит в состояние, в котором глобальная Менеджер транзакций может начать опрашивать каждого менеджера ресурсов относительно того, должен ли он продолжить и зафиксировать транзакцию. Если только один менеджер ресурсов решит, что не хочет фиксировать данные,тогда менеджер транзакций скажет всем остальным откатить свои транзакции. Однако, если все менеджеры ресурсов сообщают, что они счастливы совершить транзакцию, и они делают это посредством своего ответа на строку 7, тогда менеджер транзакций скажет всем менеджерам ресурсов, чтобы они зафиксировали свою локальную транзакцию, используя команду, подобную этой, на линии 9. После запуска строки 9 данные становятся доступны каждому, как демонстрирует оператор выбора в строке 11.данные доступны каждому, как показывает оператор выбора в строке 11.данные доступны каждому, как показывает оператор выбора в строке 11. 

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

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

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

Есть еще один важный шаг в протоколе двухфазной фиксации, а именно восстановление, которое должно быть реализовано в случаях сбоя. Когда менеджер транзакций или менеджер ресурсов становится недоступным, задача менеджеров транзакций состоит в том, чтобы продолжать попытки, пока в конечном итоге вся система снова не станет согласованной. Для этого он может запросить у менеджера ресурсов найти транзакции, которые менеджер ресурсов считает незавершенными. В Mysql соответствующая команда показана в листинге 2 вместе с ее результатом, а именно, что транзакция не завершена. Я выполнил эту команду перед командой фиксации в листинге 1. После фиксации набор результатов пуст, поскольку менеджер ресурсов очищает после себя и удаляет успешную транзакцию.

mysql> XA RECOVER ;
+----------+--------------+--------------+----------+
| formatID | gtrid_length | bqual_length | data     |
+----------+--------------+--------------+----------+
|        1 |            8 |            0 | someTxId |
+----------+--------------+--------------+----------+

Листинг 2: команда восстановления XA в SQL

дизайн

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

Рассмотрим, например, NFR-22 и MMF, систему-эквайер. Как правило, платежные системы позволяют вам резервировать деньги, а затем вскоре бронируют. Идея состоит в том, что вы звоните в их службу, чтобы убедиться, что есть свободные средства, и резервируете некоторую сумму, затем вы завершаете все свои бизнес-операции на своей стороне, а затем обязательно бронируете зарезервированные деньги, как только ваши данные будут зафиксированы (см.  FAQ). для альтернативы). Бронирование и, безусловно, бронирование должно занять не более нескольких секунд. Резервирование и снятие резервирования в случае аварии должно занять не более нескольких минут. По моему опыту, то же самое относится и к системам бронирования билетов, где билет может быть забронирован и вскоре подтвержден, когда вы готовы взять на себя ответственность за его стоимость. Я буду называть начальный этап  исполнением,  а последний этап —  коммитом . Конечно, если вы не можете завершить бизнес на своей стороне, для отмены бронирования может быть использована альтернатива второму этапу, а именно  откат . Если вы не достаточно дружелюбны, чтобы откатиться, и вы просто оставляете бронирование открытым, поставщик должен в конечном итоге разрешить бронирование тайм-аут,  чтобы зарезервированный «ресурс» (деньги или билеты в этом примере) можно было использовать в другом месте, например, чтобы покупатель мог пойти по магазинам или билет мог быть куплен кем-то другим. 

Именно такую ​​систему мы хотим связать в нашей транзакции. Договор на такие услуги выглядит следующим образом:

  1. Поставщик должен сделать доступными три операции: выполнение, фиксация и откат (хотя фиксация на самом деле необязательна  1 ),
  2. Поставщик может разрешить тайм-ауты невыполненных и без отката, после чего любые зарезервированные ресурсы могут быть использованы в других транзакциях,
  3. Успешное выполнение гарантирует, что администратору транзакций разрешено фиксировать или откатывать зарезервированные ресурсы, если не истекло время ожидания  2 ,
  4. Вызов для фиксации или отката резервирования может быть выполнен несколько раз без побочных эффектов (подумайте о идемпотентности здесь), чтобы менеджер транзакций мог завершить транзакцию, если первоначальная попытка не удалась.

Сноска # 1: Иногда веб-сервисы предлагают операцию выполнения и операцию отмены вызова, например, чтобы деньги действительно не снимались со счета клиента. Но они не предлагают операцию для совершения казни. Если мы вернемся к обсуждению вокруг листинга 2, где я заявил, что транзакции в конечном итоге непротиворечивы, а не сразу непротиворечивы, становится ясно, что не имеет значения, если система в глобальной транзакции определенно резервирует ресурсы на этапе выполнения, а не ожидает до этапа фиксации. В конце концов, либо все системы также выполнят фиксацию, либо все откатятся, и денежная транзакция будет отменена, высвободив зарезервированные средства на счете клиента. Тем не менее, обратите внимание, что сервис, предлагающий все три операции, является более чистым, и, если возможно, повлиять на дизайн системыРекомендуется убедиться, что интегрируемые сервисы предлагают все три операции: выполнение, принятие и откат. 

Сноска № 2. После успешного вызова операции execute веб-служба не может отказать в фиксации или откате транзакции в соответствии с бизнес-правилами. Он может только временно потерпеть неудачу из-за технических проблем, и в этом случае менеджер транзакций может попытаться завершить снова вскоре после этого. Недопустимо встраивать бизнес-правила в операции фиксации или отката. Вся проверка должна быть завершена во время выполнения, т.е. до времени фиксации или отката. То же самое верно в мире баз данных — во время транзакций XA база данных должна проверять все ограничения не позднее, чем на этапе подготовки, то есть, безусловно, перед этапом фиксации или отката.

Давайте сравним использование такого контракта с использованием базы данных. Возьмите веб-сервис эквайера: деньги, которые зарезервированы во время исполнения, действительно откладываются на одну сторону и больше не доступны другим организациям, пытающимся создать транзакцию по кредитной карте. Но деньги также не были переведены на наш счет. Существует три состояния: i) деньги находятся в кредит клиента; ii) деньги зарезервированы и не могут быть использованы другими транзакциями; iii) деньги забронированы и больше не доступны для клиента. Это аналогично транзакции базы данных: i) строка еще не вставлена; ii) строка вставлена, но в настоящее время не видна другим транзакциям (хотя это зависит от уровня изоляции транзакции); iii) наконец, строка предана и видна всем.Хотя это похоже на пример получателя, транзакция, которая резервирует деньги в веб-сервисе, сразу же становится видимой всему миру после того, как этап выполнения зафиксирован — она ​​не остается невидимой до окончания этапа фиксации, как в случае с базой данных. Уровень изоляции различен, но, конечно, веб-сервис может быть создан для сокрытия такой информации, например, на основе состояния, если этого требуют требования. 

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

Эти различия дают универсальному коннектору преимущество перед традиционным двухфазным фиксированием. Что действительно происходит в этом соединителе, так это то, что мы поддерживаем распределенную транзакцию, выбирая лучшие части, а именно выполнение, фиксацию, откат и восстановление. Делая это на сервере приложений Java EE, мы получаем управление транзакциями бесплатно! 

Завершающий этап, а именно  восстановление  (см. Листинги 1 и 2), необходим, но он не обязательно должен быть реализован веб-службой, поскольку адаптер может обрабатывать эту часть внутренне — ведь он знает о состоянии транзакций, так как он звонил в веб-сервис. 

Таким образом, с учетом вышеизложенных предположений мы можем создать универсальный адаптер ресурса JCA, который отслеживает состояние транзакции и вызывает операции фиксации / отката в веб-службах в нужное время, когда менеджер транзакций сообщает ресурсу XA о таких вещах, как запуск транзакции, выполнить бизнес-код и зафиксировать или откатить транзакцию.

Применимость к микросервисным архитектурам

Микросервисные архитектуры или действительно, у SOA есть одна примечательная проблема по сравнению с монолитными приложениями, а именно, что трудно поддерживать согласованность данных в распределенной системе. Микросервис обычно предоставляет операции для выполнения работы, но также должен предлагать операции для отмены этой работы. Работу не нужно делать невидимой, но ее нужно отменить в том, что касается бизнеса, чтобы в работу больше не вкладывались ресурсы (деньги, время, человеческие усилия и т. Д.). Адаптер, представленный здесь, можно использовать внутри «прикладного уровня», то есть сервиса, который вызывают ваши клиенты (мобильные, полнофункциональные веб-клиенты и т. Д.). Этот уровень следует развернуть на сервере приложений Java EE и использовать универсальный соединитель каждый раз, когда вызывается один из микросервисов в вашей среде. Таким образом, если что-то не получается,все вызовы микросервиса могут быть «откатаны» менеджером транзакций. Задача прикладного уровня состоит в том, чтобы контролировать глобальную транзакцию, чтобы все, что необходимо сделать последовательно, могло контролироваться и координироваться менеджером транзакций, вместо того, чтобы говорить, вызывая каждый микросервис непосредственно от клиента, а затем писать код, который очищает и восстанавливает последовательность.

Использование адаптера

Первым требованием, которое я дал себе, было создание API, который позволит вам добавлять бизнес-вызовы в веб-сервис внутри существующей транзакции. В листинге 3 приведен пример того, как связать вызов веб-службы с транзакцией с использованием лямбда-выражений Java 8 (даже если API совместим с Java 1.6 — пример см. В Github).

@Stateless
public class SomeServiceThatBindsResourcesIntoTransaction {

  @Resource(lookup = "java:/maxant/BookingSystem")
  private TransactionAssistanceFactory bookingFactory;
...
  public String doSomethingInvolvingSeveralResources(String refNumber) {
...
    BookingSystem bookingSystem = new BookingSystemWebServiceService()
                                        .getBookingSystemPort();
...
    try ( ...
      TransactionAssistant bookingTransactionAssistant = 
                                bookingFactory.getTransactionAssistant();
... ) {
      //NFR-34 write sales data to Oracle using JDBC and XA-Driver
      ...

      //NFR-08 book tickets
      String bookingResponse = 
          bookingTransactionAssistant.executeInActiveTransaction(txid -> {

        return bookingSystem.reserveTickets(txid, refNumber);
      });
...
      return response;
    } catch (...) {...}
...
  }
...

Листинг 3: Привязка вызова веб-сервиса к транзакции

Строка 1 обозначает класс как EJB, который по умолчанию использует транзакции, управляемые контейнером, и требует, чтобы транзакция присутствовала при каждом вызове метода, начиная с единицы, если таковой не существует. Строки 4-5 гарантируют, что экземпляр соответствующего класса адаптера ресурса будет внедрен в сервис. Строка 9 создает новый экземпляр клиента веб-службы. Этот код клиента был сгенерирован с использованием  wsimport определения службы WSDL. Строки 13-14 создают «помощника по транзакциям», который делает адаптер ресурсов доступным. Затем помощник используется в строке 21 для запуска строки 23 внутри транзакции. Это позволяет настроить ресурс XA, который менеджер транзакций использует для фиксации или отката соединения. Строка 23 возвращает значение,  String которое устанавливает  String строку 20 синхронно. 

Сравните этот код с записью в базу данных: строки 4 и 5 подобны введению a  DataSource или  EntityManager; строки 9 и 13-14 аналогичны открытию соединения с базой данных; наконец, строки 21-23 подобны вызову для выполнения некоторого SQL. 

Строка 23 не обрабатывает ошибки. Если веб-служба выдает исключение, это приводит к откату транзакции. Если вы решите перехватить такое исключение, вам нужно помнить, чтобы либо выдать другое исключение, чтобы контейнер откатывал транзакцию, либо вам нужно настроить откат транзакции, вызвав  setRollbackOnly()контекст сеанса (демонстрационный код на Github показывает пример где ловит  SQLException). 

Таким образом, накладные расходы на связывание вызова веб-службы в транзакции очень малы и аналогичны выполнению некоторого SQL в базе данных. Важно отметить, что фиксация или откат не видны в коде приложения выше. Однако нам все еще нужно показать серверу приложений, как выполнять фиксацию и откат. Это делается только один раз для каждого веб-сервиса, как показано в листинге 4.

@Startup
@Singleton
public class TransactionAssistanceSetup {
...
  @Resource(lookup = "java:/maxant/BookingSystem")
  private TransactionAssistanceFactory bookingFactory;
...
  @PostConstruct
  public void init() {
    bookingFactory
      .registerCommitRollbackRecovery(new Builder()
      .withCommit( txid -> {
        new BookingSystemWebServiceService()
          .getBookingSystemPort().bookTickets(txid);
      })
      .withRollback( txid -> {
        new BookingSystemWebServiceService()
          .getBookingSystemPort().cancelTickets(txid);
      })
      .build());
...
  }

  @PreDestroy
  public void shutdown(){
        bookingFactory.unregisterCommitRollbackRecovery();
...
  }

Листинг 4: Одноразовая регистрация обратных вызовов для обработки фиксации и отката

Здесь строки 1-2 сообщают серверу приложений, что нужно создать одиночный файл и сделать это сразу после запуска приложения. Это важно, так что если адаптеру ресурсов требуется восстановить потенциально незавершенные транзакции, он может сделать это, как только будет готов. Строки 5-6 аналогичны приведенным в листинге 3. В строке 11 мы регистрируем обратный вызов с помощью адаптера ресурсов, чтобы получить знания о том, как фиксировать и откатывать транзакции в веб-службе. Я также использовал здесь лямбды Java 8, но если вы используете Java 6/7, вы можете использовать анонимный внутренний класс вместо нового компоновщика в строке 11. Строки 13-14 просто вызывают веб-сервис, чтобы забронировать билеты, которые ранее были зарезервировано, в строке 23 листинга 3. Строки 17-18 отменяют зарезервированные билеты, если менеджер транзакций решит откатить глобальную транзакцию. Очень важно,строка 26 отменяет регистрацию обратного вызова для экземпляра адаптера при завершении работы приложения. Это необходимо, поскольку адаптер позволяет зарегистрировать только один обратный вызов на каждое имя JNDI (веб-служба), и если приложение было перезапущено без отмены регистрации обратного вызова, строка 11 завершится с ошибкой во второй раз, когда зарегистрирован обратный вызов. 

Как вы можете видеть, связывание веб-службы или чего-либо, что не поддерживает транзакции, с глобальной транзакцией JTA очень легко с помощью созданного мной универсального адаптера. Осталось только настроить адаптер так, чтобы он мог быть развернут вместе с вашим приложением.

Конфигурация адаптера

Адаптер необходимо настроить один раз для каждой веб-службы, которую он должен связать в транзакции. Чтобы сделать это немного понятнее, рассмотрим код в листинге 4 для регистрации обратных вызовов для фиксации и отката. Для каждого экземпляра адаптера может быть зарегистрирован только один обратный вызов,   то есть имя JNDI. Настройка адаптера зависит от сервера приложений, но только из-за того, где вы поместили следующий XML. В Jboss EAP 6 / Wildfly 8 и выше он помещается  <jboss-install-folder>/standalone/configuration/standalone.xmlмежду тегами XML, аналогично <subsystem xmlns="urn:jboss:domain:resource-adapters:...>

<resource-adapters>
  <resource-adapter id="GenericConnector.rar">
    <archive>
      genericconnector-demo-ear.ear#genericconnector.rar
    </archive>
    <transaction-support>XATransaction</transaction-support>
    <connection-definitions>
      <connection-definition 
          class-name=
            "ch.maxant.generic_jca_adapter.ManagedTransactionAssistanceFactory" 
          jndi-name="java:/maxant/BookingSystem" 
          pool-name="BookingSystemPool">
        <config-property name="id">
          BookingSystem
        </config-property>
        <config-property name="handleRecoveryInternally">
          true
        </config-property>
        <config-property name="recoveryStatePersistenceDirectory">
          ../standalone/data/booking-tx-object-store
        </config-property>
        <xa-pool>
          <min-pool-size>1</min-pool-size>
          <max-pool-size>5</max-pool-size>
        </xa-pool>
        <recovery no-recovery="false">
          <recover-credential>
            <user-name>asdf</user-name>
            <password>fdsa</password>
          </recover-credential>
        </recovery>
      </connection-definition>
      ... one connection-definition per registered commit/rollback callback
    </connection-definitions>
  </resource-adapter>
</resource-adapters>

Листинг 5: Настройка универсального адаптера ресурса

Листинг 5 начинается с определения адаптера ресурса в строках 2-35. Архив определяется в строке 4 — обратите внимание на символ хеша между именем файла EAR и именем файла RAR. Обратите внимание, что вам также может понадобиться указать номер версии Maven в имени файла RAR. Это зависит от физического файла в вашем EAR, и серверы приложений, отличные от JBoss, могут использовать другие соглашения. Строка 6 указывает серверу приложений использовать  XAResource адаптер from, чтобы он был связан с транзакциями XA. Строки 8-32 необходимо повторить для каждого веб-сервиса, который вы хотите интегрировать. Строки 9 и 10 определяют фабрику, которую предоставляет адаптер ресурсов, и это значение всегда должно бытьch.maxant.generic_jca_adapter.ManagedTransactionAssistanceFactory, Строка 11 определяет имя JNDI, используемое для поиска ресурса в вашем EJB. В строке 12 указывается пул, используемый для определения соединения. Рекомендуется использовать уникальное имя для каждого определения соединения. Строки 13-15 определяют идентификатор определения соединения. Вы должны использовать уникальное имя для каждого определения соединения. Строки 16-18 сообщают адаптеру ресурсов, чтобы он отслеживал внутреннее состояние транзакции, чтобы он мог обрабатывать восстановление без помощи интегрируемой веб-службы. Значение по умолчанию — false, и в этом случае вы должны зарегистрировать обратный вызов восстановления в листинге 4 — см. Листинг 6 ниже. Строки 19-21 необходимы, если адаптер ресурсов настроен для внутренней обработки восстановления — необходимо указать путь к каталогу, в который он должен записать состояние транзакции, которое он должен отслеживать.Рекомендуется использовать каталог на локальном компьютере, на котором работает сервер приложений, а не каталог, расположенный в сети. Строки 22-31 необходимы для JBoss, чтобы он действительно использовал XAResource и связывать звонки в глобальной транзакции. Возможно, что для других серверов приложений требуется только строка 6 — развертывание на других серверах приложений еще не было полностью протестировано ( подробнее … ).

восстановление

До сих пор я мало что говорил о выздоровлении. Действительно,  handleRecoveryInternally атрибут в XML в листинге 5 означает, что разработчику приложения действительно не нужно думать о восстановлении. Тем не менее, если мы вернемся к листингу 2, восстановление станет четкой частью протокола двухфазной фиксации. В самом деле, в Википедии говорится, что « для обеспечения восстановления после сбоя (в большинстве случаев автоматически) участники протокола используют протоколирование состояний протокола. Записи журнала, которые обычно генерируются медленно, но выживают при сбоях, используются процедурами восстановления протокола.«. Участниками являются менеджеры ресурсов, например, база данных или веб-сервис, или, возможно, адаптер ресурсов, если вы хотите интерпретировать это так. Я должен быть честным, я не до конца понимаю, почему менеджер транзакций не может сделать это вместо этого. Чтобы адаптер ресурсов был более гибким, а также в случае, если вам не разрешено разрешать адаптеру запись в файловую систему (отделы управления операциями в крупных корпорациях, как правило, очень строги), можно также предоставить адаптер ресурсов. с обратным вызовом, чтобы он мог запросить у веб-службы массив номеров транзакций, которые, по мнению веб-служб, находятся в неполном состоянии. Обратите внимание: если адаптер настроен, как указано выше, он отслеживает состояние вызовов веб-службы сама. Информация, которую веб-сервисВызванный метод commit или rollback сохраняется на диск после получения успешного ответа. Если сервер приложений дает сбой до того, как информация может быть записана, это не так трагично, так как адаптер сообщит диспетчеру транзакций, что транзакция не завершена, и диспетчер транзакций попытается выполнить фиксацию / откат с помощью веб-службы еще раз. Поскольку контракт веб-службы, определенный выше, требует, чтобы методы commit и rollback могли вызываться несколько раз, не вызывая проблем, не должно быть никаких проблем, когда менеджер транзакций затем пытается повторно зафиксировать или выполнить откат транзакции. Это приводит меня к утверждению, что единственная причина, по которой вы хотели бы зарегистрировать обратный вызов восстановления, заключается в том, что вам не разрешено разрешать адаптеру ресурсов запись на диск.Но я должен заявить, что не до конца понимаю, почему XA требует, чтобы менеджер ресурсов предоставил список потенциально незавершенных транзакций, когда, конечно, менеджер транзакций может сам отслеживать это состояние. 

Настройка восстановления таким образом, чтобы адаптер использовал веб-службу для запроса транзакций, которые, по его мнению, являются неполными, включает в себя сначала установку  handleRecoveryInternally атрибута в дескрипторе развертывания  false (после которого вам не нужно указывать  recoveryStatePersistenceDirectory атрибут), а затем добавление обратного вызова восстановления, как показано в листинге 6.

@Startup
@Singleton
public class TransactionAssistanceSetup {

  @Resource(lookup = "java:/maxant/Acquirer")
  private TransactionAssistanceFactory acquirerFactory;
...
  @PostConstruct
  public void init() {
    acquirerFactory
      .registerCommitRollbackRecovery(new Builder()
      .withCommit( txid -> {
...
      })
      .withRollback( txid -> {
...
      })
      .withRecovery( () -> {
        try {
          List<String> txids = new AcquirerWebServiceService().getAcquirerPort().findUnfinishedTransactions();
          txids = txids == null ? new ArrayList<>() : txids;
          return txids.toArray(new String[0]);
        } catch (Exception e) {
          log.log(Level.WARNING, "Failed to find transactions requiring recovery for acquirer!", e);
          return null;
        }
      }).build());
...    

Листинг 6: Определение обратного вызова восстановления

Регистрация обратного вызова восстановления выполняется рядом с регистрацией обратных вызовов commit и rollback, которые были настроены в листинге 4. Строки 18-26 листинга 6 добавляют обратный вызов восстановления. Здесь мы просто создаем клиент веб-службы и вызываем веб-службу, чтобы получить список идентификаторов транзакций, которые необходимо заполнить. Любые ошибки просто регистрируются, так как менеджер транзакций скоро придет и спросит снова. Более надежное решение может предпочесть сообщить администратору, если здесь есть ошибка, потому что, во-первых, здесь не должно возникать ошибок, а во-вторых, менеджер транзакций вызывает этот обратный вызов из фоновой задачи, где ни одному пользователю никогда не будет показана ошибка. Конечно, если веб-служба в настоящее время недоступна, менеджер транзакций не получит идентификаторы транзакций,но есть надежда, что в следующий раз (примерно каждые две минуты в JBoss Wildfly 8) веб-сервис снова будет доступен.

тесты

Для тестирования адаптера было создано демонстрационное приложение, основанное на сценарии, описанном в начале этой статьи (также доступном на Github), которое вызывает три веб-службы и выполняет запись в базу данных дважды, причем все во время одной и той же транзакции. Приобретатель поддерживает выполнение, фиксацию, откат и восстановление; система бронирования поддерживает выполнение, фиксацию и откат; писатель писем поддерживает только выполнение и откат. В тесте процесс сначала записывает данные в базу данных, затем вызывает получателя, затем систему бронирования, затем средство записи писем и, наконец, обновляет базу данных. Таким образом, сбой в нескольких точках процесса может быть проверен. Адаптер был протестирован с использованием следующих тестовых случаев:

  • Positive Testcase — здесь все разрешено проходить. После этого проверяются журналы, база данных и веб-службы, чтобы убедиться, что все действительно зафиксировано.
  • Сбой в конце процесса из-за нарушения ограничения внешнего ключа базы данных — здесь все веб-сервисы выполнили свою бизнес-логику, и тест гарантирует, что после сбоя базы данных менеджер транзакций откатывает вызовы веб-службы.
  • Сбой во время выполнения веб-службы эквайера — здесь, сбой происходит после начальной вставки базы данных, чтобы проверить, выполняется ли откат вставки
  • Сбой во время выполнения веб-службы бронирования — в данном случае сбой происходит после первоначальной вставки базы данных и вызова веб-службы для покупателя, чтобы проверить, что оба варианта откатились
  • Сбой во время выполнения веб-службы средства записи писем — здесь происходит сбой после начальной вставки базы данных и двух вызовов веб-службы, чтобы проверить, что все три откатаны
  • Во время фиксации веб-сервисы закрываются — устанавливая точки останова в обратных вызовах фиксации, мы можем отменить развертывание веб-сервисов и затем позволить процессу продолжаться. Первоначальная фиксация на веб-сервисах завершается неудачно, но база данных в порядке и данные доступны. Но после повторного развертывания и запуска веб-служб менеджер транзакций снова пытается выполнить фиксацию, которая должна быть успешной.
  • Во время фиксации база данных закрывается — также с использованием точек останова база данных закрывается непосредственно перед фиксацией. Коммит работает на веб-сервисах, но не работает в базе данных. После перезапуска базы данных в следующий раз, когда менеджер транзакций выполнит восстановление, он должен зафиксировать базу данных.
  • Убить сервер приложений во время подготовки перед фиксацией — здесь мы проверяем, что ничего не фиксируется.
  • Убить сервер приложений во время фиксации. Здесь мы проверяем, что после перезапуска сервера и запуска восстановления восстанавливается согласованность во всех системах, т.е. все фиксируется.

Полученные результаты

Демонстрационное приложение и адаптер ресурсов регистрируют все, что они делают, поэтому первым портом вызова является чтение журналов во время каждого теста. Кроме того, база данных записывает на диск, поэтому мы можем использовать клиент базы данных для запроса состояния базы данных, например  select * from person p inner join address a on a.person_FK = p.id;. Приобретатель пишет в папку  ~/temp/xa-transactions-state-acquirer/. Там файл с именем  exec*txt существует, если транзакция не завершена, в противном случае он называется  commit*txt или  rollback*txt если он был зафиксирован или откат соответственно. Система бронирования пишет в папку  <jboss-install>/standalone/data/bookingsystem-tx-object-store/. Автор письма пишет в папку  <jboss-install>/standalone/data/letterwriter-tx-object-store/. Адаптер удаляет временный файл с именем exec*txt после того, как транзакция будет зафиксирована или откатана, единственный способ проверить завершение — это прочитать журналы адаптера, хотя проверка удаления файлов имеет смысл, хотя и не сообщает о том, была ли зафиксирована фиксация или откат. 

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

часто задаваемые вопросы

  • Сервис, который я интегрирую, предлагает только операцию для выполнения и операцию для отмены. Нет операции фиксации.  Не беспокойтесь — это приемлемо и обсуждалось выше, где обсуждается  контракт, который должны выполнять веб-сервисы  . По сути, вызывайте операцию execute во время обычной бизнес-обработки и операцию отмены только в случае отката. На этапе фиксации ничего не делайте, поскольку данные уже были зафиксированы во время вызова операции execute.
  • Что произойдет, если веб-службе потребуется много времени для возврата в оперативный режим после выполнения бизнес-операции, но до вызова операции фиксации / отката? Транзакции, требующие восстановления, могут оказаться в затруднительном положении, если им потребуется много времени для возврата в оперативный режим, поскольку рекомендуется, чтобы системы, стоящие за веб-службой, использовали тайм-аут, после которого они очищают зарезервированные, но не зарезервированные (зафиксированные) ресурсы. Возьмем пример, где место зарезервировано в театре во время исполнения, но окончательное бронирование места задерживается из-за сбоя системы. Вполне возможно, что место освободится, скажем, через полчаса, чтобы его можно было продать другим потенциальным клиентам. Если рабочее место освобождается, и через некоторое время сервер приложений, который зарезервировал его, пытается забронировать рабочее место, может возникнуть несогласованность в системе в целом, поскольку другие участники глобальной транзакции могут быть зафиксированы, указывая, что рабочее место было продано,и, например, деньги были взяты за место, но место было продано другому клиенту. Этот случай может возникнуть в обычных двухфазных процессах фиксации.  Представьте себе транзакцию базы данных, которая создает ссылку на внешний ключ для записи, но эта запись удаляется в другой транзакции. Обычно решение состоит в том, чтобы заблокировать ресурсы, которые фактически выполняет резервирование места. Но неопределенная блокировка ресурсов может привести к таким проблемам, как взаимоблокировки. Эта проблема не уникальна для решения, представленного здесь.
  • Почему вы не рекомендуете WS-AT?  Главным образом потому, что мир полон сервисов, которые не предлагают поддержку WS-AT. А адаптер, который я здесь написал, достаточно универсален, чтобы вы могли интегрировать ресурсы, не относящиеся к веб-сервисам. Но также из-за проблем с блокировкой и временными проблемами, связанных с сохранением транзакции открытой между этапами выполнения и фиксации.
  • Почему бы просто не создать реализацию  XAResource  и включить ее в транзакцию, используя enlistResource метод?  Потому что это не помогает восстановлению. Представленный здесь универсальный соединитель также выполняет восстановление при сбое ресурса или сервера приложений во время фиксации / отката.
  • Это безумие — я не хочу реализовывать операции фиксации и отката на моих веб-сервисах!  WS-AT для вас! Или противоречивый пейзаж …
  • Я нахожусь в микросервисном ландшафте — вы можете мне помочь?  Да! Вместо того, чтобы позволить клиенту вызывать несколько микросервисов, а затем беспокоиться о глобальной согласованности данных, скажем, в случае сбоя одного вызова службы, сделайте так, чтобы клиент вызывал «уровень приложения», то есть службу, которая выполняется в приложении Java EE Север. Эта служба должна выполнять вызовы на стороне сервера с использованием универсального соединителя, и таким образом сложная логика, необходимая для обеспечения согласованности глобальных данных, обрабатывается менеджером транзакций, а не кодом, который вы должны были бы написать в противном случае.
  • Система, которую я интегрирую, требует, чтобы я вызывал ее методы commit и rollback не только с идентификатором транзакции.  Вам необходимо сохранить контекстные данные, которые вы используете на этапе выполнения, и использовать идентификатор транзакции в качестве ключа, который затем можно использовать для поиска этих данных во время фиксации, отката или восстановления. Сохраняйте данные, используя внутреннюю транзакцию ( @RequiresNew), так что данные обязательно сохраняются перед началом фиксации / отката / восстановления — таким образом, они устойчивы к сбоям.
  • Система, которую я интегрирую, определяет идентификатор сеанса и не принимает идентификатор транзакции.  Смотрите предыдущий ответ — сопоставьте идентификатор транзакции с идентификатором сеанса интегрируемой системы. Убедитесь, что вы делаете это постоянно, чтобы ваше приложение могло выжить после сбоев.
  • Платежная система, которую я интегрирую, выполняет платеж на своем веб-сайте, но «принятие» происходит через HTTP-вызов. Могу ли я интегрировать это?  Да! Перенаправить на их сайт, чтобы сделать платеж; когда они возвращаются на ваш сайт, запустите вашу бизнес-логику в транзакции и с помощью помощника по транзакциям запустите метод no-op на этапе выполнения, который вызовет обратный вызов commit во время фиксации; при обратном вызове совершить HTTP-вызов платежной системе для подтверждения платежа.

Использование универсального адаптера в вашем проекте

Чтобы использовать адаптер в вашем приложении, вам нужно сделать следующее:

  • Создать зависимость от  ch.maxant:genericconnector-api модуля Maven,
  • Напишите код, как показано в листинге 3, для выполнения бизнес-операций на веб-сервисах, которые интегрирует ваше приложение,
  • Настройте обратные вызовы фиксации и отката, как показано в листинге 4, и, при необходимости, обратный вызов восстановления, как показано в листинге 6,
  • Настройте адаптер ресурсов, как показано в листинге 5
  • Разверните адаптер ресурсов в EAR, добавив зависимость к модулю Maven ch.maxant:genericconnector-rar и указав его в качестве модуля соединителя в  application.xmlдескрипторе развертывания.

Для получения дополнительной информации см.  Демонстрационное приложение .

Выводы

Идея, которая у меня возникла, а именно связать вызовы веб-сервисов с транзакциями JTA с помощью универсального адаптера ресурсов JCA, работает. Это устраняет необходимость в создании собственной логики управления транзакциями и обеспечивает согласованность по всему ландшафту независимо от того, зафиксирована ли транзакция или откатана в коде приложения, запущенном на сервере приложений Java EE.

Дальнейшее чтение

Простое введение в теорему CAP на английском языке Возможная
последовательность и компромиссы, необходимые для распределенной разработки
Скрытые затраты на микросервисные
компромиссы
Starbucks не использует двухфазную фиксацию

С благодарностью  Клоду Гексу  за его обзор.