Мы часто создаем приложения, которым необходимо выполнять несколько из следующих действий: вызывать (микро) сервисы, записывать в базу данных, отправлять сообщение JMS и т. Д. Но что происходит, если во время вызова одного из них возникает ошибка удаленные ресурсы, например, в случае сбоя вставки базы данных после вызова веб-службы? Если удаленный вызов службы записывает данные, вы можете оказаться в глобально несовместимом состоянии, поскольку служба зафиксировала свои данные, но вызов базы данных не был зафиксирован. В таких случаях вам нужно будет компенсировать ошибку, и обычно управление этой компенсацией является чем-то сложным и написанным от руки.
Арун Гупта из Red Hat пишет о различных шаблонах микросервисов в
DZone Начало работы с микросервисами Refcard . Действительно, большинство из этих шаблонов показывают микросервис, вызывающий множество других микросервисов. Во всех этих случаях глобальная согласованность данных становится актуальной, т. Е. Обеспечение того, что сбой в одном из последних вызовов к микросервису либо компенсируется, либо повторяется попытка передачи вызова, пока все данные во всех микросервисах снова не станут согласованными. , В других статьях о микросервисах часто отсутствует упоминание о непротиворечивости данных между удаленными границами или вообще нет упоминаний, например, хорошая статья под названием « Микросервисы — это не бесплатный обед », где автор просто затрагивает проблему с утверждением « когда что-то должно произойти» … С точки зрения транзакций… все становится сложнее, когда нам приходится управлять… распределенными транзакциями, связывающими различные действия ». Действительно, мы делаем, но в таких статьях не упоминается, как это сделать.
Традиционный способ управления согласованностью в распределенных средах заключается в использовании распределенных транзакций. Для наблюдения за тем, чтобы глобальная система оставалась последовательной, создается диспетчер транзакций. Протоколы, такие как двухфазная фиксация, были разработаны для стандартизации процесса. JTA, JDBC и JMS — это спецификации, которые позволяют разработчикам приложений поддерживать согласованность нескольких баз данных и серверов сообщений. JCA — это спецификация, которая позволяет разработчикам создавать оболочки для корпоративных информационных систем (EIS). А в недавней статье я написал о том, как я построил универсальный JCA-коннектор, который позволяет вам связывать такие вещи, как вызовы микросервисов, с этими глобальными распределенными транзакциями, точно так, чтобы вам не приходилось писать свой собственный каркасный код для обработки сбоев во время распределенные транзакции. Соединитель позаботится о том, чтобы ваши данные в конечном итоге были согласованными .
Но у вас не всегда будет доступ к полноценному серверу приложений Java EE, который поддерживает JCA, особенно в микросервисной среде, и поэтому я теперь расширил библиотеку, включив автоматическую обработку commit / rollback / recovery в следующих средах:
- Весенний ботинок
- Spring + Tomcat / Jetty
- Сервлеты + Tomcat / Jetty
- Весенняя партия
- Автономные Java-приложения
Чтобы сделать это, приложения должны использовать JTA-совместимый менеджер транзакций, а именно один из Atomikos или Bitronix .
Следующее описание опирается на тот факт, что вы прочитали предыдущую статью в блоге .
Процесс настройки удаленного вызова таким образом, чтобы он был зачислен в транзакцию, аналогичен использованию адаптера JCA, представленного в предыдущей статье блога. Есть два шага: 1) вызов удаленной службы внутри обратного вызова, переданного объекту TransactionAssistant
полученному из класса BasicTransactionAssistanceFactory
, и 2) настройка центрального обработчика фиксации / отката.
Первый шаг, а именно код, принадлежащий стадии выполнения (см. Предыдущую статью в блоге), выглядит следующим образом (при использовании Spring):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@Service @Transactional public class SomeService { @Autowired @Qualifier ( "xa/bookingService" ) BasicTransactionAssistanceFactory bookingServiceFactory; public String doSomethingInAGlobalTransactionWithARemoteService(String username) throws Exception { //write to say a local database... //call a remote service String msResponse = null ; try (TransactionAssistant transactionAssistant = bookingServiceFactory.getTransactionAssistant()){ msResponse = transactionAssistant.executeInActiveTransaction(txid->{ BookingSystem service = new BookingSystemWebServiceService().getBookingSystemPort(); return service.reserveTickets(txid, username); }); } return msResponse; } } |
Листинг 1. Вызов веб-службы внутри транзакции
Строки 5-6 предоставляют экземпляр фабрики, использованной в строке 13 для получения TransactionAssistant
. Обратите внимание, что вы должны убедиться, что используемое здесь имя совпадает с именем, использованным во время установки в листинге 3 ниже. Это связано с тем, что когда транзакция фиксируется или откатывается, администратору транзакций необходимо найти соответствующий обратный вызов, используемый для фиксации или компенсации вызова, сделанного в строке 16. Более чем вероятно, что в вашем приложении будет несколько таких удаленных вызовов, как этот и для каждой интегрируемой удаленной службы вы должны написать код, подобный показанному в листинге 1. Обратите внимание, что этот код не отличается от использования JDBC для вызова базы данных. Для каждой базы данных, которую вы включаете в транзакцию, вам необходимо:
- ввести источник данных (аналогично строкам 5-6)
- получить соединение от источника данных (строка 13)
- создать заявление (строка 14)
- выполнить инструкцию (строки 15-16)
- закрыть соединение (строка 13, когда блок try вызывает метод close автоматически закрываемого ресурса). Очень важно закрыть помощник по транзакциям после того, как он был использован, прежде чем транзакция будет завершена.
Чтобы создать экземпляр BasicTransactionAssistanceFactory
(строки 5-6 в листинге 1), мы используем Spring @Configuration
:
01
02
03
04
05
06
07
08
09
10
11
|
@Configuration public class Config { @Bean (name= "xa/bookingService" ) public BasicTransactionAssistanceFactory bookingSystemFactory() throws NamingException { Context ctx = new BitronixContext(); BasicTransactionAssistanceFactory microserviceFactory = (BasicTransactionAssistanceFactory) ctx.lookup( "xa/bookingService" ); return microserviceFactory; } ... |
Листинг 2: Spring @Configuration
, используемый для создания фабрики
В строке 4 листинга 2 используется то же имя, что и в @Qualifier
в строке 5 листинга 1. Метод в строке 5 листинга 2 создает фабрику, просматривая ее в JNDI, в этом примере с использованием Bitronix. Код выглядит немного иначе при использовании Atomikos — подробности смотрите в проекте demo/genericconnector-demo-springboot-atomikos
.
Второй шаг, упомянутый выше, заключается в настройке обратного вызова в режиме фиксации / отката. Это будет использоваться менеджером транзакций, когда транзакция вокруг строк 8-20 в листинге 1 фиксируется или откатывается. Обратите внимание, что транзакция происходит из-за аннотации @Transactional
в строке 2 листинга 1. Эта настройка показана в листинге 3:
01
02
03
04
05
06
07
08
09
10
11
12
|
CommitRollbackCallback bookingCommitRollbackCallback = new CommitRollbackCallback() { private static final long serialVersionUID = 1L; @Override public void rollback(String txid) throws Exception { new BookingSystemWebServiceService().getBookingSystemPort().cancelTickets(txid); } @Override public void commit(String txid) throws Exception { new BookingSystemWebServiceService().getBookingSystemPort().bookTickets(txid); } }; TransactionConfigurator.setup( "xa/bookingService" , bookingCommitRollbackCallback); |
Листинг 3: Настройка обработчика фиксации / отката
Строка 12 передает обратный вызов в конфигуратор вместе с тем же уникальным именем, которое использовалось в списках 1 и 2.
Фиксация в строке 9 вполне может быть пустой, если интегрируемая служба предлагает только метод выполнения и компенсационный метод для этого выполнения. Этот обратный вызов фиксации происходит из двухэтапного фиксации, целью которого является сохранение времени, в течение которого распределенные системы не соответствуют абсолютному минимуму. Смотрите обсуждение в конце этой статьи.
Строки 5 и 9 создают новый клиент веб-службы. Обратите внимание, что обработчик обратного вызова не должен иметь состояния ! Он сериализуем, потому что на некоторых платформах, например Atomikos, он будет сериализован вместе с транзакционной информацией, чтобы при необходимости его можно было вызывать во время восстановления. Я полагаю, вы могли бы сделать это с состоянием, пока он остается сериализуемым, но я рекомендую оставить это без сохранения состояния.
Идентификатор транзакции (строка с именем txid
), переданный txid
вызову в строках 4 и 8, передается веб-службе в этом примере. В более реалистичном примере вы использовали бы этот идентификатор для поиска контекстной информации, сохраненной на этапе выполнения (см. Строки 15 и 16 в листинге 1). Затем вы будете использовать эту контекстную информацию, например, ссылочный номер, полученный при более раннем обращении к веб-службе, чтобы выполнить вызов для фиксации или отката вызова веб-службы, приведенного в листинге 1.
Автономные варианты этих списков, например, для использования этой библиотеки вне среды Spring, практически идентичны, за исключением того, что вам нужно управлять транзакцией вручную. Посмотрите demo
папку на Github для примеров кода в нескольких поддерживаемых средах.
Обратите внимание, что в версии универсального соединителя JCA вы можете настроить, будет ли универсальный соединитель обрабатывать восстановление внутренне. Если это не так, вы должны предоставить обратный вызов, который может вызвать менеджер транзакций, чтобы найти транзакции, которые, по вашему мнению, еще не завершены. В обсуждении, не связанном с JCA, который обсуждается в этой статье, это всегда обрабатывается внутренним соединителем. Общий соединитель запишет контекстную информацию в каталог и использует ее во время восстановления, чтобы сообщить менеджеру транзакций, что необходимо очистить. Строго говоря, это не совсем правильно, потому что если ваш жесткий диск выходит из строя, вся информация о незавершенных транзакциях будет потеряна. При строгой двухфазной фиксации администратору транзакций разрешается обращаться к ресурсу, чтобы получить список незавершенных транзакций, требующих восстановления. В современном мире RAID-контроллеров нет причин, по которым производственный компьютер должен когда-либо терять данные из-за сбоя жесткого диска, и по этой причине в настоящее время нет возможности предоставить обратный вызов универсальному соединителю, который может сообщить ему, какие транзакции находятся в состояние, которое нуждается в восстановлении. В случае катастрофического аппаратного сбоя узла, когда невозможно было снова запустить и запустить узел, вам необходимо физически скопировать все файлы, которые записывает общий соединитель, со старого жесткого диска на второй узел. Диспетчер транзакций и универсальный соединитель, работающий на втором узле, будут затем работать согласованно для завершения всех зависших транзакций, либо фиксируя их, либо откатывая их, в зависимости от того, что имело значение во время сбоя. Этот процесс не отличается от копирования журналов диспетчера транзакций во время аварийного восстановления, в зависимости от того, какой диспетчер транзакций вы используете. Шансы на то, что вам когда-либо понадобится сделать это, очень малы — за всю свою карьеру я никогда не знал производственную машину из проекта / продукта, над которым я работал, чтобы потерпеть неудачу таким образом.
Вы можете настроить, где эта контекстная информация записывается, используя второй параметр, показанный в листинге 4:
1
|
MicroserviceXAResource.configure(30000L, new File( "." )); |
Листинг 4: Настройка универсального соединителя. Показанные значения также являются значениями по умолчанию.
В листинге 4 устанавливается минимальный срок транзакции, прежде чем она станет релевантной для восстановления. В этом случае транзакция будет считаться релевантной для очистки с помощью восстановления, если срок ее действия превышает 30 секунд. Вам может потребоваться настроить это значение в зависимости от времени, которое требуется для выполнения вашего бизнес-процесса, и это может зависеть от суммы периодов времени ожидания, настроенных для каждой внутренней службы, которую вы вызываете. Существует компромисс между низким значением и высоким значением: чем меньше значение, тем меньше времени требуется фоновой задаче, выполняемой в диспетчере транзакций для очистки во время восстановления после сбоя. Это означает, что чем меньше значение, тем меньше окно несогласованности. Но будьте осторожны, если значение слишком низкое, задача восстановления попытается откатить транзакции, которые на самом деле все еще активны. Обычно вы можете настроить период ожидания менеджера транзакций, и значение, указанное в листинге 4, должно быть более чем равно периоду ожидания менеджера транзакций. Кроме того, каталог, в котором хранятся контекстные данные, настроен в листинге 4 как локальный каталог. Вы можете указать любой каталог, но убедитесь, что каталог существует, потому что универсальный соединитель не будет пытаться его создать.
Если вы используете Bitronix в среде Tomcat, вы можете обнаружить, что не так много информации о том, как настроить среду. Раньше это очень хорошо документировалось до того, как Bitronix был перенесен с codehaus.org на Github. Я создал проблему с Bitronix для улучшения документации. Исходный код и файл readme в папке demo/genericconnector-demo-tomcat-bitronix
содержат подсказки и ссылки.
Последнее, что следует отметить при использовании универсального соединителя, — это как работают коммит и откат. Все, что делает соединитель, это копирование поверх транзакции JTA, так что в случае, если что-то нужно откатить, он получает уведомление посредством обратного вызова. Затем универсальный соединитель передает эту информацию в ваш код в обратном вызове, который зарегистрирован в листинге 3. Фактический откат данных в серверной части не является чем-то, что делает универсальный соединитель — он просто вызывает ваш обратный вызов, чтобы вы могли попросите серверную систему откатить данные. Обычно вы не выполняете откат как таковой, скорее вы помечаете записанные данные как недействительные, как правило, с использованием состояний. Может быть очень трудно правильно откатить все следы данных, которые уже были записаны на этапе выполнения. При строгой настройке протокола двухфазной фиксации, например, с использованием двух баз данных, данные, записанные в каждом ресурсе, остаются в заблокированном состоянии, недоступными сторонним транзакциям, между выполнением и фиксацией / откатом. Действительно, это один из недостатков двухфазного принятия, поскольку блокировка ресурсов снижает масштабируемость. Обычно интегрируемая серверная система не блокирует данные между фазой выполнения и фазой фиксации, и действительно, обратный вызов при фиксации останется пустым, потому что ничего не нужно делать — данные, как правило, уже зафиксированы во внутренней части, когда строка 16 Листинга 1 возвращается на этапе выполнения. Однако если вы хотите построить более строгую систему и влиять на реализацию интегрируемой серверной части, тогда данные в серверной системе могут быть «заблокированы» между этапами выполнения и фиксации, как правило, с помощью состояний Например, «Билет зарезервирован» после выполнения и «Билет забронирован» после принятия. Сторонним транзакциям не будет разрешен доступ к ресурсам / билетам в «зарезервированном» состоянии.
- Общий коннектор и ряд демонстрационных проектов доступны по адресу https://github.com/maxant/genericconnector/, а двоичные файлы и источники доступны от Maven .
Ссылка: | Глобальная согласованность данных, транзакции, микросервисы и Spring Boot / Tomcat / Jetty от нашего партнера JCG Антона Кучера в блоге «Кухня в зоопарке» . |