Статьи

Глобальная согласованность данных, транзакции, микросервисы и Spring Boot / Tomcat / Jetty

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

Арун Гупта из Red Hat пишет о различных шаблонах микросервисов в  DZone Начало работы с микросервисной рефкартой . Действительно, большинство из этих шаблонов показывают микросервис, вызывающий множество других микросервисов. Во всех этих случаях глобальная согласованность данных становится актуальной, т. Е. Гарантируется, что сбой в одном из последних вызовов к микросервису либо компенсируется, либо повторяется попытка передачи вызова, пока все данные во всех микросервисах снова не станут согласованными. , В других статьях о микросервисах часто отсутствует упоминание о непротиворечивости данных между удаленными границами или вообще нет упоминаний, например, хорошая статья под названием « Микросервисы — это не бесплатный обед », где автор просто затрагивает проблему с утверждением ».когда что-то должно случиться … транзакционно … все усложняется, когда нам приходится управлять … распределенными транзакциями, связывающими различные действия. «Да, действительно, но в таких статьях не упоминается, как это сделать. 

Традиционный способ управления согласованностью в распределенных средах заключается в использовании распределенных транзакций. Для наблюдения за тем, чтобы глобальная система оставалась последовательной, создается диспетчер транзакций. Протоколы, такие как двухфазное принятие были разработаны для стандартизации процесса. 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):

@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 автоматически закрываемого ресурса). Это очень важно , чтобы закрыть помощник сделки  после того, как она была использована, прежде чем транзакция будет завершена.

Для того, чтобы создать экземпляр theBasicTransactionAssistanceFactory

 (строки 5-6 в листинге 1), мы используем Spring  @Configuration:

@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:

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), переданный обратному вызову в строках 4 и 8, передается веб-службе в этом примере. В более реалистичном примере вы использовали бы этот идентификатор для поиска контекстной информации, сохраненной на этапе выполнения (см. Строки 15 и 16 в листинге 1). Затем вы будете использовать эту контекстную информацию, например, ссылочный номер, полученный при более раннем обращении к веб-службе, чтобы выполнить вызов для фиксации или отката вызова веб-службы, приведенного в листинге 1. 

Автономные варианты этих списков, например, для использования этой библиотеки вне среды Spring, практически идентичны, за исключением того, что вам нужно управлять транзакцией вручную. См.  demo Папку на Github для примеров кода в нескольких поддерживаемых средах. 

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

Вы можете настроить, где эта контекстная информация записывается, используя второй параметр, показанный в листинге 4:

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