Стратегии работы с транзакциями: Распространенные ошибки

Не забывайте о распространенных ошибках при реализации транзакций на платформе Java

В процессе выполнения транзакций должна достигаться высокая степень целостности и согласованности данных. В этой статье, первой части серии, посвященной вопросам эффективной стратегии выполнения транзакций на платформе Java, Марк Ричардс описывает распространенные подводные камни, которые могут привести к потере согласованности. Часто совершаемые ошибки объясняются на примерах, взятых из инфраструктуры Spring Framework и спецификации EJB (Enterprise JavaBeans) 3.0.

Марк Ричардс, директор и старший технический архитектор, Collaborative Consulting, LLC

Mark RichardsМарк Ричардс (Mark Richards) – директор и старший технический архитектор в компании Collaborative Consulting, LLC. Он является автором второго издания книги "Java Message Service" (O'Reilly, 2009 г.), а также "Java Transaction Design Strategies" (C4Media Publishing, 2006 г.). Кроме того, он участвовал в написании ряда других книг, в том числе "97 Things Every Software Architect Should Know" (O'Reilly, 2009 г.), "NFJS Anthology Volume 1" (Pragmatic Bookshelf, 2006 г.) и "NFJS Anthology Volume 2" (Pragmatic Bookshelf, 2007 г.). Он обладает сертификатами архитектора и разработчика от компаний IBM, Sun, The Open Group и BEA. Он регулярно выступает с докладами на симпозиумах серии "No Fluff Just Stuff", а также на других конференциях и собраниях пользователей по всему миру.



02.06.2011

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

Об этой серии

Транзакции позволяют повысить качество, целостность и согласованность данных, а также надежность приложения в целом. При этом реализация успешной схемы выполнения транзакций в приложениях Java представляет собой нетривиальную задачу, основная сложность которой заключается в проектировании, а не в написании кода. В этой новой серии Марк Ричардс предлагает свое руководство по дизайну эффективной стратегии управления транзакциями при решении различных задач – от создания простых приложений до реализации высокопроизводительных транзакционных систем.

Разработчики, не желающие применять транзакции, поступают так на свой страх и риск. Практически во всех бизнес-приложениях необходимо поддерживать высокое качество данных. Один только финансовый инвестиционный бизнес теряет десятки миллиардов долларов из-за неудачных торгов, причем второй по распространенности причиной этого являются некачественные данные (см. раздел Ресурсы). Отсутствие транзакций является не единственным фактором, отвечающим за потерю качества данных, но тем не менее можно смело утверждать, что недостаточная и отсутствующая вовсе поддержка транзакций стоит финансовой индустрии миллиардов долларов.

Одной из проблем является плохая осведомленность разработчиков о транзакциях вообще. Мне часто приходится слышать утверждения наподобие такого: "В наших приложениях не бывает сбоев, поэтому поддержка транзакций не требуется". В чем-то это верно. Я встречал приложения, которые или очень редко, или никогда не выбрасывают исключения. Благодаря хорошо написанному коду, процедурам валидации, а также циклу тестирования и проверке покрытия кода тестами, им удается избежать потерь производительности и повышенной сложности, связанной с обработкой транзакций. Однако подобный взгляд на необходимость транзакций является несколько однобоким, поскольку принимает во внимание только одно их свойство – атомарность. Атомарность гарантирует, что все изменения в базе данных являются единым целым и либо все выполняются, либо все отменяются. Однако координированное выполнение и откат изменений – это лишь один из аспектов транзакционности. Другим аспектом является изоляция транзакций, означающая, что процедуры работы с данными изолируются друг от друга. В отсутствие должной изоляции одним процедурам могут стать видимы изменения, сделанные другими, параллельно выполняющимися процедурами, даже если последние еще не завершились. В результате бизнес-решения могут приниматься на основании неполных данных, которые, в частности, могут привести к срыву торгов и другим негативным и дорого обходящимся последствиям.

Лучше поздно, чем никогда

В полной мере я начал оценивать проблемы, связанные с обработкой транзакций, в начале 2000-х годов, когда, работая на стороне заказчика, натолкнулся на строчку в проектном плане, которая была последней перед задачей тестирования системы. Она гласила: "Реализовать поддержку транзакций. Да-да, это ведь так просто – добавить поддержку транзакций в крупное приложение, практически готовое к тестированию, не правда ли? К сожалению, этот подход встречается слишком часто. По крайней мере, в том проекте, в отличие от большинства проектов, была заложена поддержка транзакций, пусть и в самом конце цикла разработки.

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

Несмотря на то, что в большей части примеров к этой статье используется Spring Frameworks (версия 2.5), понятия при работе с транзакциями остались такими же, как и в спецификации EJB 3.0. В большинстве случаев достаточно заменить аннотацию @Transactional в Spring на аннотацию @TransactionAttribute из спецификации EJB 3.0. В тех случаях, когда Spring и EJB расходятся (концептуально и технически), будут приведены примеры кода, соответствующие обеим инфраструктурам.

Ошибки, связанные с локальными транзакциями

Начать лучше всего с рассмотрения самого простого случая, а именно – локальных транзакций, также часто называемых транзакциями баз данных. В былые времена при работе с хранилищами данных (например, через JDBC) было принято оставлять обработку транзакций базе данных. В конце концов, это ее прямая обязанность, не так ли? Локальные транзакции идеально подходят для обработки логических единиц работы (logical units of work – LUW), выполняющих единичные операторы вставки, изменения или удаления данных. Например, рассмотрим простой фрагмент JDBC-кода, который добавляет торговый приказ в таблицу TRADE (листинг 1).

Листинг 1. Простая вставка записи в базу данных через JDBC
@Stateless
public class TradingServiceImpl implements TradingService {
   @Resource SessionContext ctx;
   @Resource(mappedName="java:jdbc/tradingDS") DataSource ds;

   public long insertTrade(TradeData trade) throws Exception {
      Connection dbConnection = ds.getConnection();
      try {
         Statement sql = dbConnection.createStatement();
         String stmt =
            "INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
          + "VALUES ("
          + trade.getAcct() + "','"
          + trade.getAction() + "','"
          + trade.getSymbol() + "',"
          + trade.getShares() + ","
          + trade.getPrice() + ",'"
          + trade.getState() + "')";
         sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
         ResultSet rs = sql.getGeneratedKeys();
         if (rs.next()) {
            return rs.getBigDecimal(1).longValue();
         } else {
            throw new Exception("Trade Order Insert Failed");
         }
      } finally {
         if (dbConnection != null) dbConnection.close();
      }
   }
}

В коде работы через JDBC, приведенном в листинге 1, нет никакой логики обработки транзакций, хотя они и сохраняют торговые приказы в таблице TRADE в базе данных. В данном случае за выполнение транзакций отвечает сама база данных.

Все это работает хорошо в случае выполнения единственной операции в LUW. Однако, допустим, что необходимо обновить состояние счета в момент добавления заказа в базу данных, как показано в листинге 2.

Листинг 2. Выполнение нескольких операций изменения данных внутри одного метода
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //запись в журнал ошибок
      throw up;
   }
}

В этом примере методы insertTrade() и updateAcct() используют JDBC, не включая их в одну транзакцию. После выполнения метода insertTrade() торговый приказ был сохранен в базе данных, и соответствующая транзакция была подтверждена. Если вызов метода updateAcct(), то приказ останется в таблице TRADE что будет означать несогласованность базы данных. Если бы метод placeTrade() использовал транзакции, то обе операции являлись бы частью единой LUW, и заказ на торги был бы отменен в случае сбоя при обновлении счета.

Подобные примеры кода, непосредственно работающего с JDBC, стали встречаться реже в связи с распространением Java-инфраструктур для работы с хранилищами данных, таких как Hibernate, TopLink и Java Persistence API (JPA). Как правило, используются библиотеки для объектно-реляционного отображения (ORM), которые облегчают жизнь разработчикам, скрывая весь нелицеприятный код JDBC за вызовами нескольких методов. Например, для того, чтобы выполнить ту же операцию по вставке заказа, JDBC-код из листинга 1 можно заменить на JPA-код, показанный в листинге 3, предварительно отобразив класс TradeData на таблицу TRADE.

Листинг 3. Пример простой вставки данных при помощи JPA
public class TradingServiceImpl {
    @PersistenceContext(unitName="trading") EntityManager em;

    public long insertTrade(TradeData trade) throws Exception {
       em.persist(trade);
       return trade.getTradeId();
    }
}

Обратите внимание, что в листинге 3 вызывается метод persist() класса EntityManager для вставки торгового приказа. Все просто? Отнюдь. Этот код не вставит запись в таблицу TRADE, как этого можно было бы ожидать, но не выбросит и исключения. Он просто вернет значение 0 в качестве первичного ключа приказа, не изменяя состояния базы данных. Это один из первых серьезных подводных камней при обработке транзакций: инфраструктурам ORM требуются транзакции для синхронизации содержимого кэша объектов и базы данных. Генерация SQL-кода и изменение базы данных вследствие выполнения нужного оператора (insert, update или delete) выполняется только при подтверждении транзакции. В отсутствие транзакции некому дать сигнал ORM о том, что надо сгенерировать код SQL и выполнить изменения в базе данных, поэтому метод просто завершается, ничего не сделав, даже не выбросив исключения. При работе с ORM вы обязаны использовать транзакции. Больше нельзя полагаться на базу данных в вопросах управления соединения и подтверждения изменений.

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


Ошибки, связанные с аннотацией @Transactional в Spring Framework

Итак, выполнив код, приведенный в листинге 3, мы убедились, что в отсутствие транзакций метод persist() работает не так, как предполагалось. Выполнив поверхностный поиск в Интернет, можно найти ряд страниц, на которых объясняется, что при работе с Spring Framework следует использовать аннотацию @Transactional. Добавьте ее в код, как показано в листинге 4.

Листинг 4. Пример использования аннотации @Transactional
public class TradingServiceImpl {
   @PersistenceContext(unitName="trading") EntityManager em;

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {
      em.persist(trade);
      return trade.getTradeId();
   }
}

Однако этот вариант программы по-прежнему не работает по причине того, что следует явно сообщить Spring Framework о факте использования аннотаций для управления транзакциями. Эту проблему иногда сложно обнаружить, если код полностью не покрыт юнит-тестами. Из-за этого некоторые разработчики попросту добавляют логику управления транзакциями в конфигурационные файлы Spring вместо использования аннотаций.

При использовании аннотации @Transactional в Spring следует добавить следующую строку в конфигурационный файл Spring:

<tx:annotation-driven transaction-manager="transactionManager"/>

В свойстве transaction-manager хранится ссылка на менеджер транзакций, определенный в конфигурационном файле Spring. Эта строка указывает Spring, что при применении перехватчика транзакций следует использовать аннотацию @Transactional. Без нее данная аннотация игнорируется, в результате чего транзакции в коде не используются вовсе.

Добиться того, чтобы аннотация @Transactional в листинге 4 заработала – это только начало. Обратите внимание, что в листинге 4 эта аннотация используется без каких бы то ни было параметров. Мне встречалось множество разработчиков, которые применяли аннотацию @Transactional, не разобравшись полностью в том, что она делает. Например, какой режим распространения используется при отсутствии параметров аннотации? Каково значение флага "только чтение" (read-only)? Какой уровень изоляции используется? Еще более важным является вопрос о том, когда транзакция должна откатываться. Понимание того, как следует применять данную аннотацию, очень важно для организации правильной поддержки транзакций в вашем приложении. Ответы звучат следующим образом: при использовании аннотации @Transactional без параметров режимом распространения является REQUIRED, значением атрибута "только чтение" – false, уровень изоляции соответствует уровню изоляции по умолчанию для базы данных (как правило, это READ_COMMITTED), и транзакция не будет откатываться в случае контролируемых исключений (checked exception).


Ошибки, связанные с флагом "только чтение" аннотации @Transactional

Одной из ошибок, которые мне часто приходится наблюдать, является некорректное использование признака "только чтение" аннотации @Transactional в Spring. Попробуйте ответить на следующий вопрос: что делает аннотация @Transactional (листинг 5) в случае установки этого флага в значение true и режима распространения SUPPORTS при использовании стандартного JDBC-кода для сохранения данных?

Листинг 5. Пример использования флага "только чтение" и режима распространения SUPPORTS с JDBC
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
   //Работа через JDBC...
}

Выберите из следующих вариантов правильный вариант того, что произойдет при выполнении метода insertTrade() из листинга 5.

  • Выбросит исключение, поскольку подключение разрешает только операции чтения.
  • Корректно вставит торговый приказ и подтвердит сохранение данных.
  • Ничего не сделает, поскольку уровнем распространения является SUPPORTS.

Сдаетесь? Правильным является ответ "В". Торговый приказ корректно сохраняется в базе данных несмотря на флаг "только чтение" и режим распространения SUPPORTS. Как это возможно? Дело в том, что из-за режима распространения SUPPORTS новая транзакция не начинается, поэтому в методе используется локальная транзакция (т.е. транзакция базы данных). Флаг "только чтение" применяется только в случае начала новой транзакции, а в этом примере он попросту игнорируется.

Тогда возникает вопрос, что будет делать аннотация @Transactional в листинге 6 с установленным флагом "только чтение" и режимом распространения REQUIRED?

Листинг 6. Пример использования флага "только чтение" и режима распространения REQUIRED в JDBC
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   //Работа через JDBC...
}

Как вы думаете, что из следующего произойдет при выполнении метода insertTrade(), приведенного в листинге 6:

  • Метод выбросит исключение, поскольку подключение разрешает только операции чтения.
  • Метод корректно вставит торговый приказ и подтвердит сохранение данных.
  • Ничего не произойдет, поскольку выставлен флаг "только чтение".

На этот вопрос несложно ответить, принимая во внимание объяснение предыдущей ситуации. Правильным ответом является "А". Метод выбросит исключение, говорящее о том, что производится попытка изменения данных через соединение, разрешающее только операции чтения. В данном случае начинается новая транзакция (из-за режима распространения REQUIRED), поэтому соединение помечается как "только для чтения". Таким образом, при попытке выполнить оператор SQL вы получите исключение, указывающее на это.

Странной особенностью флага "только чтение" является то, что он вступает в силу только с началом новой транзакции. Зачем надо начинать транзакцию, если речь идет только о чтении данных? Разумеется, этого делать не нужно. Начало транзакции при выполнении операции чтения всего лишь добавит лишних накладных расходов в обрабатывающий поток, а также может привести к появлению разделяемых блокировок чтения в базе данных (это зависит от типа используемой базы данных и уровня изоляции). Таким образом, флаг "только чтение" оказывается несколько бессмысленным при работе через JDBC и приводит к увеличению накладных расходов из-за выполнения необязательной транзакции.

А что будет происходить при использовании ORM-инфраструктуры? Попробуйте угадать результат работы аннотации @Transactional в случае, если метод insertTrade() вызывался при использовании Hibernate и JPA (листинг 7).

Листинг 7. Пример использования флага "только чтение" и режима распространения REQUIRED в JPA
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   return trade.getTradeId();
}

Выберите правильный вариант того, что произойдет при вызове метода insertTrade() из листинга 7.

  • Метод выбросит исключение, поскольку подключение разрешает только операции чтения.
  • Метод корректно вставит торговый приказ и подтвердит сохранение данных.
  • Ничего не произойдет, поскольку выставлен флаг readOnly (только чтение).

Ответ на этот вопрос не так прост. В некоторых ситуациях ответом будет "С", однако в большинстве случаев (в частности, при использовании JPA) – "В". Торговый приказ благополучно добавляется в базу данных без всяких ошибок. Секундочку, ведь предыдущий пример ясно продемонстрировал, что при использовании режима распространения REQUIRED будет выброшено исключение. Это так при работе через JDBC, однако при использовании инфраструктуры ORM флаг "только чтение" работает несколько по-другому. В момент вставки записи инфраструктура будет сначала обращаться к базе данных для генерации ключевого значения. В некоторых ORM режим сброса данных (flush) будет равен MANUAL, поэтому, если ключ не сгенерирован, то вставка выполнена не будет. То же самое справедливо для изменения данных. Однако другие ORM, в частности TopLink, всегда выполняют вставку и изменение данных при установленном флаге "только чтение". Несмотря на то, что это поведение определяется конкретной инфраструктурой ORM и ее версией, необходимо помнить, что нет гарантии, что операции вставки или изменения данных не будут выполнены при установленном флаге "только чтение", особенно при использовании JPA, поскольку ее поведение не зависит от компании-разработчика.

Итак, мы подходим к еще одной ошибке, которую мне часто приходится наблюдать. Приняв во внимание все, что было рассказано выше, как вы думаете, что произойдет при выполнении кода в листинге 8, где единственным атрибутом аннотации @Transactional является флаг "только чтение"?

Листинг 8. Пример использования флага "только чтение" в JPA
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

Что из следующего произойдет при выполнении метода getTrade(), приведенного в листинге 8.

  • Транзакция начнется, будет выбран торговый приказ, затем транзакция будет подтверждена.
  • Торговый приказ будет получен без старта новой транзакции.

Никогда не говори "никогда"

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

Правильным ответом является "А": транзакция будет начата и подтверждена. Не забывайте, что режимом распространения по умолчанию для аннотации @Transactional является REQUIRED. Это означает, что транзакция будет начата даже в том случае, когда это не необходимо (см. заметку Никогда не говори "никогда"). В зависимости от используемой базы данных, это может приводить к необязательным разделяемым блокировкам, которые могут повлечь за собой взаимные блокировки. Кроме того, начало и завершение транзакции приводят к увеличению продолжительности обработки и повышенному расходованию ресурсов. Итак, можно сделать следующий вывод: при использовании ORM флаг "только чтение", как правило, бесполезен, и в большинстве случаев игнорируется. Однако если вы все же хотите его использовать, то обязательно установите режим распространения SUPPORTS, чтобы избежать запуска транзакций (листинг 9).

Листинг 9. Пример использования флага "только чтение" и режима распространения SUPPORTS при выборке данных
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

Еще лучше вообще не использовать аннотацию @Transactional при чтении данных из базы (листинг 10).

Листинг 10. Выборка данных без аннотации @Transactional
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

Ошибки, связанные с атрибутом транзакций REQUIRES_NEW

При работе со Spring Framework или EJB использование атрибута транзакций REQUIRES_NEW может привести к негативным результатам, в частности, к искажению данных или потере согласованности. Этот атрибут всегда запускает новую транзакцию при начале выполнения метода вне зависимости от того, существует ли ранее начатая транзакция. Многие разработчики некорректно используют атрибут REQUIRES_NEW, предполагая, что это правильный способ гарантии запуска транзакции. Рассмотрим два метода, показанные в листинге 11.

Листинг 11. Пример использования атрибута REQUIRES_NEW
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}

Заметьте, что оба метода в листинге 11 являются открытыми, а это, в частности, означает, что они могут быть вызваны независимо друг от друга. Проблемы, связанные с атрибутом REQUIRES_NEW, возникают в случае, если методы вызываются внутри одной логической единицы работы (LUW) путем межсервисного или программного взаимодействия. Например, допустим, что метод updateAcct() в листинге 11 может вызываться независимо от других методов, однако в некоторых случаях он может быть вызван внутри метода insertTrade(). Если при этом будет выброшено исключение после вызова updateAcct(), то добавление торгового приказа будет отменено, но изменения баланса счета будут сохранены в базу данных (листинг 12).

Листинг 12. Несколько операций изменения данных при использовании атрибута REQUIRES_NEW
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   updateAcct(trade);
   //В этом месте возникает исключение.
   //Добавление приказа отменяется, а изменение баланса - нет.
   ...
}

Это происходит потому, что новая транзакция начинается внутри метода updateAcct() и, следовательно, она подтверждается после его благополучного завершения. Использование атрибута транзакции REQUIRES_NEW означает, что выполнение существующей транзакции (если таковая есть) прерывается, и начинается новая транзакция. После завершения метода новая транзакция подтверждается, а старая – возобновляется.

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


Ошибки, связанные с откатом транзакций

Я приберег наиболее распространенную ошибку напоследок. К сожалению, мне приходится видеть ее в рабочем коде в более чем половине случаев. Мы сначала рассмотрим ситуацию с использованием Spring Framework, а затем перейдем к EJB 3.

До этого момента все примеры кода выглядели примерно как в листинге 13.

Листинг 13. Выполнение операций без возможности отката
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //Запись ошибки в журнал
      throw up;
   }
}

Допустим, что на счете недостаточно средств для покупки ценных бумаг, либо он не настроен должным образом для их покупки или продажи. В этом случае метод выбросит контролируемое исключение, например FundsNotAvailableException. Сохранится ли в базе данных торговый приказ, или логическая единица работы будет полностью отменена? Как ни странно, но при выбросе контролируемого исключения в Spring Framework или EJB транзакция подтвердит все неподтвержденные к этому моменту изменения. Применительно к листингу 13 это означает, что при возникновении исключения в методе updateAcct() торговый приказ будет сохранен, но соответствующие изменения в балансе счета произведены не будут.

Это, возможно, главная угроза целостности и согласованности данных при работе с транзакциями. Исключения времени выполнения (или неконтролируемые исключения) автоматически приводят к откату всей логической единицы работы, а контролируемые исключения – нет. Таким образом, код в листинге 13 оказывается бессмысленным с точки зрения обработки транзакций. Может показаться, что транзакции используются для обеспечения атомарности и согласованности, но это лишь видимость.

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

Если вы используете декларативную модель транзакций (она более подробно описывается во второй статье этой серии), то необходимо указать, как именно контейнер или инфраструктура должны обрабатывать контролируемые исключения. В Spring Framework это делается при помощи параметра rollbackFor в аннотации @Transactional (листинг 14).

Листинг 14. Добавление возможности отката транзакции при работе с Spring
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //Запись ошибки в журнал
      throw up;
   }
}

Обратите внимание на использование параметра rollbackFor в аннотации @Transactional. Значением этого параметра может быть либо класс исключения, либо массив подобных классов. Кроме того, можно использовать параметр rollbackForClassName для задания имен исключений в строковом виде. Существует также обратный параметр noRollbackFor для указания того, что любое исключение, кроме заданных, должно приводить к откату транзакции. Как правило, большинство разработчиков указывают в качестве значения Exception.class, что означает, что все исключения, выбрасываемые данным методом, должны приводить к откату транзакции.

В том, что касается отката транзакций, поведение EJB несколько отличается от Spring Framework. Атрибут @TransactionAttribute, описанный в спецификации EJB 3.0, не предоставляет директив для управления откатом транзакции. Вместо этого вы должны использовать метод SessionContext.setRollbackOnly() для того, чтобы пометить транзакцию как подлежащую откату (листинг 15).

Листинг 15. Добавление возможности отката транзакций при работе с EJB
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //Запись ошибки в журнал
      sessionCtx.setRollbackOnly();
      throw up;
   }
}

После вызова метода setRollbackOnly() уже ничего изменить нельзя: откат неминуемо произойдет после завершения выполнения метода, который начал транзакцию. В следующих статьях будут описаны стратегии обработки транзакций, которые помогут вам решить, когда и где использовать директивы отката, а также в каких случаях следует использовать атрибуты транзакции REQUIRED и MANDATORY.


Заключение

Код, который приходится писать для выполнения транзакций в Java, не представляет особых сложностей, но при этом бывает непросто решить, как его использовать и конфигурировать. Вследствие этого с реализацией поддержки транзакционности в приложениях Java связано множество распространенных ошибок (в том числе некоторые реже встречающиеся проблемы, которые не вошли в эту статью). Главной проблемой является то, что о некорректной работе транзакций в вашем приложении вы не узнаете ни от ошибок компилятора, ни от аварийного завершения программы на этапе выполнения. Более того, реализация поддержки транзакций не ограничивается лишь написанием кода, как это предполагалось в истории, описанной в заметке "Лучше поздно, чем никогда". Это также требует существенных усилий на этапе проектирования приложения. Остальные статьи серии "Стратегии работы с транзакциями" помогут вам в проектировании эффективных стратегий работы с транзакциями при решении различных задач – от создания простых приложений до реализации высокопроизводительных транзакционных систем.

Ресурсы

Научиться

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=676754
ArticleTitle=Стратегии работы с транзакциями: Распространенные ошибки
publish-date=06022011