![image](https://habrastorage.org/webt/ek/1w/6p/ek1w6pyygmendtlfezucuvzecai.jpeg)
Книга, предназначенная для разработчиков и архитекторов из больших корпораций, рассказывает, как проектировать и писать приложения в духе микросервисной архитектуры. Также в ней описано, как делается рефакторинг крупного приложения — и монолит превращается в набор микросервисов.
Предлагаем ознакомиться с отрывком «Управление транзакциями в микросервисной архитектуре»
Почти любой запрос, обрабатываемый промышленным приложением, выполняется в рамках транзакции базы данных. Разработчики таких приложений используют фреймворки и библиотеки, которые упрощают работу с транзакциями. Некоторые инструменты предоставляют императивный API для выполняемого вручную начала, фиксации и отката транзакций. А такие фреймворки, как Spring, имеют декларативный механизм. Spring поддерживает аннотацию @Transactional, которая автоматически вызывает метод в рамках транзакции. Благодаря этому написание транзакционной бизнес-логики становится довольно простым.
Если быть более точным, управлять транзакциями просто в монолитных приложениях, которые обращаются к единой базе данных. Если же приложение задействует несколько БД и брокеров сообщений, этот процесс затрудняется. Ну а в микросервисной архитектуре транзакции охватывают несколько сервисов, каждый из которых имеет свою БД. В таких условиях приложение должно использовать более продуманный механизм работы с транзакциями. Как вы вскоре увидите, традиционный подход к распределенным транзакциям нежизнеспособен в современных приложениях. Вместо него системы на основе микросервисов должны применять повествования.
Но прежде, чем переходить к повествованиям, посмотрим, почему управление транзакциями создает столько сложностей в микросервисной архитектуре.
4.1.1. Микросервисная архитектура и необходимость в распределенных транзакциях
Представьте, что вы — разработчик в компании FTGO и отвечаете за реализацию системной операции createOrder(). Как было написано в главе 2, эта операция должна убедиться в том, что заказчик может размещать заказы, проверить детали заказа, авторизовать банковскую карту заказчика и создать запись Order в базе данных. Реализация этих действий была бы относительно простой в монолитном приложении. Все данные, необходимые для проверки заказа, уже готовы и доступны. Кроме того, для обеспечения согласованности данных можно было бы использовать ACID-транзакции. Вы могли бы просто указать аннотацию @Transactional для метода сервиса createOrder().
Однако выполнить эту операцию в микросервисной архитектуре гораздо сложнее. Как видно на рис. 4.1, данные, необходимые операции createOrder(), разбросаны по нескольким сервисам. createOrder() считывает информацию из сервиса Consumer и обновляет содержимое сервисов Order, Kitchen и Accounting.
Поскольку у каждого сервиса есть своя БД, вы должны использовать механизм для согласования данных между ними.
4.1.2. Проблемы с распределенными транзакциями
Традиционный подход к обеспечению согласованности данных между несколькими сервисами, БД или брокерами сообщений заключается в применении распределенных транзакций. Стандартом де-факто для управления распределенными транзакциями является X/Open XA (см. ru.wikipedia.org/wiki/XA). Модель XA использует двухэтапную фиксацию (two-phase commit, 2PC), чтобы гарантировать сохранение или откат всех изменений в транзакции. Для этого требуется, чтобы базы данных, брокеры сообщений, драйверы БД и API обмена сообщениями соответствовали стандарту XA, необходим также механизм межпроцессного взаимодействия, который распространяет глобальные идентификаторы XA-транзакций. Большинство реляционных БД совместимы с XA, равно как и некоторые брокеры сообщений. Например, приложение на основе Java EE может выполнять распределенные транзакции с помощью JTA.
![image](https://habrastorage.org/webt/ff/wu/yn/ffwuynn5jzi8tq1syefgykvayi0.png)
Несмотря на внешнюю простоту, распределенные транзакции имеют ряд проблем. Многие современные технологии, включая такие базы данных NoSQL, как MongoDB и Cassandra, их не поддерживают. Распределенные транзакции не поддерживаются и некоторыми современными брокерами сообщений вроде RabbitMQ и Apache Kafka. Так что, если вы решите использовать распределенные транзакции, многие современные инструменты будут вам недоступны.
Еще одна проблема распределенных транзакций связана с тем, что они представляют собой разновидность синхронного IPC, а это ухудшает доступность. Чтобы распределенную транзакцию можно было зафиксировать, доступными должны быть все вовлеченные в нее сервисы. Как описывалось в главе 3, доступность системы — это произведение доступности всех участников транзакции. Если в распределенной транзакции участвуют два сервиса с доступностью 99,5 %, общая доступность будет 99 %, что намного меньше. Каждый дополнительный сервис понижает степень доступности. Эрик Брюер (Eric Brewer) сформулировал CAP-теорему, которая гласит, что система может обладать лишь двумя из следующих трех свойств: согласованность, доступность и устойчивость к разделению (ru.wikipedia.org/wiki/Теорема_CAP). В наши дни архитекторы отдают предпочтение доступным системам, жертвуя согласованностью.
На первый взгляд распределенные транзакции могут показаться привлекательными. С точки зрения разработчика, они имеют ту же программную модель, что и локальные транзакции. Но из-за проблем, описанных ранее, эта технология оказывается нежизнеспособной в современных приложениях. В главе 3 было показано, как отправлять сообщения в рамках транзакции базы данных, не используя при этом распределенные транзакции. Для решения более сложной проблемы, связанной с обеспечением согласованности данных в микросервисной архитектуре, приложение должно применять другой механизм, основанный на концепции слабо связанных асинхронных сервисов. И здесь пригодятся повествования.
4.1.3. Использование шаблона «Повествование» для сохранения согласованности данных
Повествования — это механизм, обеспечивающий согласованность данных в микросервисной архитектуре без применения распределенных транзакций. Повествование создается для каждой системной команды, которой нужно обновлять данные в нескольких сервисах. Это последовательность локальных транзакций, каждая из которых обновляет данные в одном сервисе, задействуя знакомые фреймворки и библиотеки для ACID-транзакций, упомянутые ранее.
Шаблон «Повествование»
Обеспечивает согласованность данных между сервисами, используя последовательность локальных транзакций, которые координируются с помощью асинхронных сообщений. См. microservices.io/patterns/data/saga.html.
Системная операция инициирует первый этап повествования. Завершение одной локальной транзакции приводит к выполнению следующей. В разделе 4.2 вы увидите, как координация этих этапов реализуется с помощью асинхронных сообщений. Важным преимуществом асинхронного обмена сообщениями является то, что он гарантирует выполнение всех этапов повествования, даже если один или несколько участников оказываются недоступными.
Повествования имеют несколько важных отличий от ACID-транзакций. Прежде всего, им не хватает изолированности (подробно об этом — в разделе 4.3). К тому же, поскольку каждая локальная транзакция фиксирует свои изменения, для отката повествования необходимо использовать компенсирующие транзакции, о которых мы поговорим позже в этом разделе. Рассмотрим пример повествования.
Пример повествования: создание заказа
В этой главе в качестве примера используем повествование Create Order (рис. 4.2). Оно реализует операцию createOrder(). Первая локальная транзакция инициируется внешним запросом создания заказа. Остальные пять транзакций срабатывают одна за другой.
![image](https://habrastorage.org/webt/uz/fb/t7/uzfbt7k9fnld4hszv9_t3knnbr0.png)
Это повествование состоит из следующих локальных транзакций.
1. Сервис Order. Создает заказ с состоянием APPROVAL_PENDING.
2. Сервис Consumer. Проверяет, может ли заказчик размещать заказы.
3. Сервис Kitchen. Проверяет детали заказа и создает заявку с состоянием CREATE_PENDING.
4. Сервис Accounting. Авторизует банковскую карту заказчика.
5. Сервис Kitchen. Меняет состояние заявки на AWAITING_ACCEPTANCE.
6. Сервис Order. Меняет состояние заказа на APPROVED.
В разделе 4.2 я покажу, как сервисы, участвующие в повествовании, взаимодействуют между собой с помощью асинхронных сообщений. Сервис публикует сообщение по завершении локальной транзакции. Это инициирует следующий этап повествования и позволяет не только добиться слабой связанности участников, но и гарантировать полное выполнение повествования. Даже если получатель временно недоступен, брокер буферизирует сообщение до того момента, когда его можно будет доставить.
Повествования выглядят простыми, но их использование связано с некоторыми трудностями, прежде всего с нехваткой изолированности между ними. Решение проблемы описано в разделе 4.3. Еще один нетривиальный аспект связан с откатом изменений при возникновении ошибки. Посмотрим, как это делается.
Повествования применяют компенсирующие транзакции для отката изменений
У традиционных ACID-транзакций есть одно прекрасное свойство: бизнес-логика может легко откатить транзакцию, если обнаружится нарушение бизнес-правила. Она просто выполняет команду ROLLBACK, а база данных отменяет все изменения, внесенные до этого момента. К сожалению, повествование нельзя откатить автоматически, поскольку на каждом этапе оно фиксирует изменения в локальной базе данных. Это, к примеру, означает, что в случае неудачной авторизации банковской карты на четвертом этапе повествования Create Order приложение FTGO должно вручную отменить изменения, сделанные на предыдущих трех этапах. Вы должны написать так называемые компенсирующие транзакции.
Допустим, (n + 1)-я транзакция в повествовании завершилась неудачно. Необходимо нивелировать последствия от предыдущих n транзакций. На концептуальном уровне каждый из этих этапов Ti имеет свою компенсирующую транзакцию Ci, которая отменяет эффект от Ti. Чтобы компенсировать эффект от первых n этапов, повествование должно выполнить каждую транзакцию Ci в обратном порядке. Последовательность выглядит так: T1… Tn, Cn… C1 (рис. 4.3). В данном примере отказывает этап Tn + 1, что требует отмены шагов T1… Tn.
![image](https://habrastorage.org/webt/ko/87/cx/ko87cx4krtvrrtmlmubc1a2ldsg.png)
Повествование выполняет компенсирующие транзакции в обратном порядке по отношению к исходным: Cn… C1. Здесь действует тот же механизм последовательного выполнения, что и в случае с Ti. Завершение Ci должно инициировать Ci – 1.
Возьмем, к примеру, повествование Create Order. Оно может отказать по целому ряду причин.
1. Некорректная информация о заказчике, или заказчику не позволено создавать заказы.
2. Некорректная информация о ресторане, или ресторан не в состоянии принять заказ.
3. Невозможность авторизовать банковскую карту заказчика.
В случае сбоя в локальной транзакции механизм координации повествования должен выполнить компенсирующие шаги, которые отклоняют заказ и, возможно, заявку. В табл. 4.1 собраны компенсирующие транзакции для каждого этапа повествования Create Order. Следует отметить, что не всякий этап требует компенсирующей транзакции. Это относится, например, к операциям чтения, таким как verifyConsumerDetails(), или к операции authorizeCreditCard(), все шаги после которой всегда завершаются успешно.
![image](https://habrastorage.org/webt/sh/xw/6i/shxw6ir8i54rjtwehje5qrp8ckw.png)
В разделе 4.3 вы узнаете, что первые три этапа повествования Create Order называются доступными для компенсации транзакциями, потому что шаги, следующие за ними, могут отказать. Четвертый этап называется поворотной транзакцией, потому что дальнейшие шаги никогда не отказывают. Последние два этапа называются доступными для повторения транзакциями, потому что они всегда заканчиваются успешно.
Чтобы понять, как используются компенсирующие транзакции, представьте ситуацию, в которой авторизация банковской карты заказчика проходит неудачно. В этом случае повествование выполняет следующие локальные транзакции.
1. Сервис Order. Создает заказ с состоянием APPROVAL_PENDING.
2. Сервис Consumer. Проверяет, может ли заказчик размещать заказы.
3. Сервис Kitchen. Проверяет детали заказа и создает заявку с состоянием CREATE_PENDING.
4. Сервис Accounting. Делает неудачную попытку авторизовать банковскую карту заказчика.
5. Сервис Kitchen. Меняет состояние заявки на CREATE_REJECTED.
6. Сервис Order. Меняет состояние заказа на REJECTED.
Пятый и шестой этапы — это компенсирующие транзакции, которые отменяют обновления, внесенные сервисами Kitchen и соответственно Order. Координирующая логика повествования отвечает за последовательность выполнения прямых и компенсирующих транзакций. Посмотрим, как это работает.
Об авторе
Крис Ричардсон (Chris Richardson) — разработчик, архитектор и автор книги POJOs in Action (Manning, 2006), в которой описывается процесс построения Java-приложений уровня предприятия с помощью фреймворков Spring и Hibernate. Он носит почетные звания Java Champion и JavaOne Rock Star.
Крис разработал оригинальную версию CloudFoundry.com — раннюю реализацию платформы Java PaaS для Amazon EC2.
Ныне он считается признанным идейным лидером в мире микросервисов и регулярно выступает на международных конференциях. Крис создал сайт microservices.io, на котором собраны шаблоны проектирования микросервисов. А еще он проводит по всему миру консультации и тренинги для организаций, которые переходят на микросервисную архитектуру. Сейчас Крис работает над своим третьим стартапом Eventuate.io. Это программная платформа для разработки транзакционных микросервисов.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — Microservice Patterns
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.