Если вы прочтете любую ознакомительную статью или книгу по J2EE, то обнаружите, что описанию сервиса транзакций Java (JTS) и Java API для транзакций (JTA) отводится весьма незначительное место. Но это не потому, что JTS - маловажная или необязательная часть J2EE - совсем наоборот. JTS меньше освещается в печатных изданиях, чем технологии EJB, потому что сервисы, которые он предоставляет приложению, - практически невидимы; многие разработчики даже не знают, где и когда в их приложении начинаются и заканчиваются транзакции. Недостаточная изученность JTS в некотором смысле является следствием его успешности: он так успешно скрывает следы транзакций, что мы редко говорим и слышим о нем. Тем не менее, вам, возможно, интересно будет узнать, что ПО делает от вашего имени за кулисами.
Не было бы преувеличением сказать, что без транзакций создание надежного распределенного приложения было бы практически невозможным. Транзакции позволяют нам контролировать изменение постоянного состояния приложения, с тем чтобы оно было устойчивым ко всякого рода сбоям системы, включая полный отказ системы, неполадки сети, перебои в питании и даже стихийные бедствия. Транзакции - одни из основных структурных единиц, необходимых при построении ошибкоустойчивых, высоконадежных и легкодоступных приложений.
Представьте себе, что Вы переводите деньги с одного счета на другой. Баланс каждого счета представлен строкой в БД. Если Вам нужно перевести средства со счета А на счет В, Вы, скорее всего, выполните некий SQL-код, например, такой:
SELECT accountBalance INTO aBalance
FROM Accounts WHERE accountId=aId;
IF (aBalance >= transferAmount) THEN
UPDATE Accounts
SET accountBalance = accountBalance - transferAmount
WHERE accountId = aId;
UPDATE Accounts
SET accountBalance = accountBalance + transferAmount
WHERE accountId = bId;
INSERT INTO AccountJournal (accountId, amount)
VALUES (aId, -transferAmount);
INSERT INTO AccountJournal (accountId, amount)
VALUES (bId, transferAmount);
ELSE
FAIL "Insufficient funds in account";
END IF
|
На данный момент программа выглядит достаточно простой. Если на счете А достаточно средств, требуемая сумма будет снята с одного счета и добавлена на другой. Но что произойдет в случае перебоя в питании или полного сбоя системы? Записи, представляющие счета А и В, едва ли хранятся в одном дисковом блоке, а это означает, что требуется несколько операций ввода-вывода информации для осуществления перевода. Что, если сбой системы произойдет после выполнения первой, но до выполнения второй? Тогда деньги могут уйти со счета А, но не попасть на счет В (ни А, ни В не будут в восторге), или же деньги поступят на счет В, но останутся и на счете А (банк не будет в восторге). А что, если счета будут обновлены, а файл регистрации - нет? Тогда операции с денежными средствами А и В, выполненные банком в текущем месяце, не будут отвечать состоянию их счетов.
Невозможно записывать несколько блоков данных на диск одновременно, тем более, что запись каждого блока данных на диск при одновременном изменении их отрицательно скажется на работоспособности системы. Отложенное сохранение записи на диске до более удобного времени может значительно повысить производительность приложения, но при этом надо обеспечить сохранность данных.
Даже при отсутствии системных сбоев существует другая опасность - параллельный режим работы. Что, если на счете А хранится $100, и он начинает осуществлять два перевода по $100 каждый на два разных счета абсолютно одновременно? Если синхронизация по времени окажется неэффективной, при отсутствии подходящего блокирующего механизма оба перевода могут быть успешно выполнены, оставив А с отрицательным балансом.
Эти сценарии вполне вероятны, и естественно ожидать, что корпоративная система баз данных справится с ними. Мы ожидаем от своих банков правильного управления нашими счетами, даже если случатся пожар, наводнение, перебой в энергоснабжении и системные ошибки. Ошибкоустойчивость может обеспечиваться резервным оборудованием - резервными дисками, компьютерами и даже информационными центрами - но то, что позволяет построить практичное ошибкоустойчивое приложение, - это транзакции. Транзакции предоставляют основу для обеспечения целостности и надежности данных, не зависящих от системных отказов.
Итак, что же такое транзакция? Прежде чем приступить к определению этого понятия, давайте определим, что такое состояние приложения. Состояние приложения охватывает все элементы данных в оперативной памяти и на диске, влияющие на работу приложения - все, что приложение "знает." Состояние приложения может храниться в памяти, в файле или в БД. В случае сбоя системы - например, если происходит сбой в работе приложения, сети или компьютерной системы, нам надо быть уверенными в том, что при перезапуске системы состояние приложения может быть восстановлено.
Теперь мы можем определить транзакцию как совокупность взаимосвязанных операций состояния приложения, обладающую такими свойствами как атомарность, непротиворечивость, изолированность и долговечность. Все вместе эти свойства называются свойствами ACID (atomicity, consistency, isolation, durability).
Атомарность значит, что вне зависимости от того, все ли транзактные операции применены к приложению, или ни одна из них; транзакция является неделимой элементарной операцией.
Непротиворечивость означает, что транзакция представляет верное изменение состояния приложения - что любые ограничения целостности, подразумеваемые для приложения, не нарушаются транзакцией. На практике понятие непротиворечивости зависит от приложения. Например, в бухгалтерском приложении непротиворечивость значит, что суммарная величина счетов активов равна суммарной величине счетов пассивов. Мы вернемся к этому требованию при обсуждении демаркации транзакций в Части 3 данной серии.
Изолированность подразумевает, что результаты одной транзакции не влияют на другие транзакции, выполняющиеся параллельно с ней; с точки зрения транзакций это скорее выглядит как последовательное, а не одновременное выполнение. В системах БД изолированность обычно реализуется посредством блокирующего механизма. Требование изолированности иногда выполняется нестрого для определенных транзакций в целях обеспечения лучшей производительности приложения.
Долговечность значит, что при успешном выполнении транзакции изменения в состоянии приложения сохраняются при сбоях (survive failures).
Что понимается под "сохранением при сбоях?" Что представляет собой сбой, не нарушающий состояние? Это зависит от системы; для хорошо спроектированных систем четко определены те сбои, для которых возможно восстановление системы. Транзактные БД, установленные на моем рабочем компьютере, устойчивы к сбоям системы и перебоям электропитания, но не к пожару, если он сожжет дотла административное здание, где располагается мой офис. Банк, скорее всего, не только обеспечит резервные диски, сети и системы в своем информационном центре, но, возможно, создаст резервные информационные центры в разных городах, соединенные резервными коммуникациями, для восстановления в случае серьезных неприятностей, таких как стихийные бедствия. Системы данных военных организаций могут иметь еще более строгие требования к сохранению работоспособности.
Типовая транзакция имеет несколько участников - приложение, монитор обработки транзакций (TPM) и одну или более подсистему управления ресурсами (RM). RM хранит состояние приложения и в большинстве случаев представлена БД, но может быть также сервером очереди сообщений (в приложении J2EE это будет провайдер JMS) или другим транзактным ресурсом. TPM координирует действия нескольких RM для обеспечения в транзакции характеристики "все-или-ничего".
Транзакция начинается, когда приложение просит контейнер или монитор транзакции начать новую транзакцию. По мере получения доступа приложением к разным RM, они вносятся в список в транзакции. RM должен связывать любые изменения состояния приложения с транзакцией, которая их затребовала.
Транзакция завершается в одном их двух случаев: транзакция совершена приложением, или транзакция возвращена в исходное состояние либо приложением, либо вследствие сбоя в одном из RM. Если транзакция успешно завершена, изменения, связанные с ней, будут записаны в постоянное хранилище и открыты для доступа для новых транзакций. Если она была возвращена в исходное состояние, все изменения, внесенные ею будут отменены; все будет так, словно этой транзакции никогда не было.
Журнал транзакций - ключ к долговечности
Транзактные RM приобретают долговечность при сохранении приемлемой производительности благодаря тому, что собирают результаты нескольких транзакций и ведут записи этих результатов в один журнал транзакций. Журнал транзакций хранится как последовательный файл на диске (или иногда в простом сегменте) и обычно предназначен лишь для записи, а не для чтения, за исключением случаев отката транзакций или восстановления. В нашем примере банковского счета балансы, связанные со счетами А и В будут обновлены в памяти и записаны в журнал транзакций. Внесение обновленной записи в журнал транзакций требует записи на диск данных меньшего суммарного объема (необходимо записать лишь измененные данные, а не весь блок данных) и меньшего количества обращений к диску (поскольку все изменения могут содержаться в последовательных дисковых блоках журнала). Далее, изменения связанные с несколькими одновременными транзакциями могут быть скомбинированы в одну запись в журнале, что значит, что мы можем обработать несколько транзакций и делать одно сохранение на диск, вместо нескольких для каждой транзакции. Затем RM обновит существующие дисковые блоки, соответствующие измененным данным.
Восстановление при перезагрузке
В случае сбоя системы первое, что выполняется при перезапуске, - повторное применение результатов выполнения любых выполненных транзакций, запись о которых присутствует в журнале, но данные которых не были обновлены. таким образом, журнал гарантирует долговечность при сбоях, а также позволяет снизить количество выполняемых операций ввода-вывода, или по меньшей мере, перенести их на то время, когда они будут иметь меньшее воздействие на работу системы.
Двухфазный контроль выполнения транзакции
Многие транзакции задействуют лишь один RM - обычно БД. В этом случае RM обычно выполняет большую часть работы по завершению или откату транзакции. (Практически все транзактные RM имеют свой встроенные менеджер транзакций, которые может обработать локальные транзакции - транзакции, задействующие только этот RM.) Однако, если транзакция задействует два или более RM - возможно, две разные БД, или БД и очередь JMS, или два разных провайдера JMS - надо быть уверенным, что семантика "все-или-ничего" применяется не только внутри одного RM, но и между разными RM в транзакции. В этом случае TPM организует двухфазный контроль выполнения транзакции. При двухфазном контроле TPM сначала отправляет "Подготовительное" сообщение каждому RM, запрашивая, готов ли он и может ли завершить транзакцию; при получении утвердительного ответа ото всех RM, он помечает транзакцию как совершенную в своем журнале транзакций и затем поручает всем RM завершить транзакцию. Если возникает сбой в RM, при перезапуске он запросит TPM о статусе любых транзакций, ожидавших обработки на момент сбоя, и либо совершает их, либо отменяет.
Аналогом двухфазного контроля выполнения транзакции в обществе является свадебная церемония - священник или судья вначале спрашивает каждую сторону "Берешь ли ты этого мужчину / эту женщину в мужья / жены?" Если обе стороны говорят "да", объявляется о том, что они оба вступили в брак; иначе брак не состоится. Ни при каких обстоятельствах не может быть, чтобы одна сторона вступила в брак, а вторая - нет, вне зависимости от того, кто первым скажет "Я согласен".
Транзакции как механизм обработки исключительных ситуаций
Вы могли заметить, что транзакции предлагают множество таких же возможностей для данных в приложении, что и синхронизированные блокировки для данных в оперативной памяти - обеспечение атомарности, отображения изменений и прямое управление. Но в то время как синхронизация - в первую очередь, средство управления параллельным выполнением, транзакции - в первую очередь, средства обработки исключительных ситуаций. В мире, где диски никогда не выходят их строя, не бывает сбоев в работе системы и ПО, а напряжение в сети всегда неизменно 100%, мы не будем нуждаться в транзакциях. Транзакции в приложениях выполняют ту же роль, что и договорное право в обществе. Они обусловливают выполнение обязательств, если одна из сторон не в состоянии выполнить свою часть договора. При составлении договора, мы обычно надеемся, что предусмотрели все, и, слава богу, в большинстве случаев так и бывает.
Аналогом простейших программ на Java было бы предоставление транзакциями тех же
преимуществ на уровне приложения, что блоки catch и finally на уровне метода; они позволяют нам осуществлять
восстановление после ошибок без написания объемного кода для восстановления. Изучите
приведенный ниже метод, который копирует один файл в другой:
public boolean copyFile(String inFile, String outFile) {
InputStream is = null;
OutputStream os = null;
byte[] buffer;
boolean success = true;
try {
is = new FileInputStream(inFile);
os = new FileOutputStream(outFile);
buffer = new byte[is.available()];
is.read(buffer);
os.write(buffer);
}
catch {IOException e) {
success = false;
}
catch (OutOfMemoryError e) {
success = false;
}
finally {
if (is != null)
is.close();
if (os != null)
os.close();
}
return success;
}
|
Если опустить тот факт, что создание одного буфера для всего файла - не самая
блестящая идея, то что может произойти с этим методом? Да что угодно. Файл ввода
может не существовать, или данный пользователь может не иметь прав на его чтение.
Пользователь может не иметь прав на сохранение выходного файла, или же он может быть
заблокирован другим пользователем. Может не оказаться достаточно места на диске для
выполнения операции по записи файла, или размещение буфера не сможет быть выполнено
из-за нехватки памяти. К счастью, все эти проблемы могут быть решены посредством
оператора finally, освобождающего все ресурсы,
используемые copyFile().
Если бы Вы писали такой метод во времена С, для каждой операции (открытие файла ввода, открытие файла вывода, чтение, запись) Вам пришлось бы проверять состояние выдачи результата, и в случае невыполнения операции отменить все предыдущие успешно выполненные операции и вернуть соответствующее состояние программы. Программа была бы гораздо больше и потому менее читабельна вследствие большого объема кода для обработки ошибок. В части программы по обработке исключительных ситуаций (которую также труднее всего протестировать) также легче всего допустить ошибки, либо забыв освободить ресурс, либо заполнив его дважды, либо освободив еще не размещенный ресурс. А имея более сложный метод, использующий гораздо больше ресурсов, чем два файла и буфер, задача усложняется еще больше. Бывает сложно даже найти логику программы в этом коде восстановления при ошибке.
Теперь представьте, что Вы выполняете сложную операцию, включающую в себя вставку и обновление нескольких записей в нескольких БД, и одна из операций нарушает требование непротиворечивости, а поэтому не выполняется. Если бы Вы управляли собственной системой восстановления при ошибках, Вам бы пришлось отследить, какие операции Вами уже выполнены, и как отменить каждую из них, если ошибка вызвана следующей операцией. Задача становится еще более сложной, если элементарная операция распространяется на несколько компонентов или методов. Конструируя Ваше приложение с использованием транзакций, Вы передаете хранение промежуточных результатов БД - просто скажите ВЕРНУТЬ В ИСХОДНОЕ СОСТОЯНИЕ, и все, что Вы сделали с момента начала транзакции, будет отменено.
Конструируя наше приложение с использованием транзакций, мы определяем набор верных трансформаций состояния приложения и получаем гарантию того, что приложение всегда находится в корректном состоянии, даже после сбоя в работе системы или компонента. Транзакции позволяют нам возложить на TPM и RM многие операции по обработке исключительных ситуаций и восстановлению после ошибок, что упрощает программу и дает нам возможность подумать о логике приложения.
Во второй части данной серии мы изучим, какое это имеет значение для приложений J2EE - как J2EE позволяет нам делать транзактную семантику частью компонентов J2EE (компонентов EJB, сервлетов и страниц JSP); как он делает включение ресурсов в списки скрытым для приложения, даже для компонентно-управляемых транзакций; и как одна транзакция может скрытым образом следовать за процессом управления от одного компонента EJB к другому, или от сервлета к компоненту EJB, и даже между системами.
Несмотря на то, что J2EE обеспечивает достаточно скрытые сервисы транзакции объектов, разработчики приложений, все-таки вы должны быть аккуратны при демаркации транзакций и при использовании транзактных ресурсов в приложении - неправильная транзактная демаркация может привести приложение в нестабильное состояние, а неправильное использование транзактных ресурсов - стать причиной серьезных неполадок в работе программы. Мы рассмотрим эти проблемы и предложим некоторые советы по использованию Ваших транзакций в третьей части данной серии.
- Примите участие в обсуждении материала на форуме.
- Оригинал статьи: Understanding JTS - An introduction to transactions.
-
Обработка транзакций: Принципы и Технология
, Джим Грей, Андреас Рейтер. Всестороннее описание обработки
транзакций
-
Принципы обработки транзакций
, Филип Бернстейн, Эрик Ньюкамер. Детальное введение в проблему;
исторические моменты освещены так же подробно, как и сами концепты.
-
Сервис транзакций Java
(JTS): спецификация вполне читабельна и на высоком уровне объясняет, как
транзактный монитор объекта включается в распределенное приложение.
- Простое и подробное объяснение транзактной поддержки в J2EE приведено в
спецификации Java API для
транзакций (JTA).
-
Спецификация J2EE
описывает, как JTS и JTA встраиваются в J2EE, и как транзакции взаимодействуют с
другими технологиями J2EE, такими как корпоративные компоненты Enterprise
JavaBeans.
- В работе "Принципы регистрации транзакций" подробно разъяснены
реализация журнала транзакций и выполнение возврата в исходное состояние
восстановления при перезапуске.
-
Поддержка открытых стандартов для Web служб и J2EE (PDF), официальный
документ IBM, знакомящий с местом транзакций в мире Web-сервисов.
- В статье "Интерпретация качества служб для Web-сервисов"
(developerWorks, январь 2002 г.), Анбажаган Мани и Арун Нагараян
обсуждают, насколько транзакции должны соответствовать требованиям теста
ACID.
- Читайте полное собрание статей
Теория и практика Java
.
- Найдите другие статьи и учебники по Java в разделе
Java-технологии на developerWorks.
Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com