Продолжается подписка на наши издания! Вы не забыли подписаться?

Транзакционные стратегии

Автор: Марк Ричардс
Опубликовано: 12.07.2010
Версия текста: 1.1
Часть 1. Проблемы транзакций
Проблемы с локальными транзакциями
Проблемы с аннотацией @Transactional в Spring Framework
Проблемы с флагом read-only аннотации @Transactional
Проблемы с атрибутом транзакции REQUIRES_NEW
Проблемы с откатом транзакций
Заключение
Часть 2. Обзор моделей и стратегий
Транзакционные стратегии
Заключение

Часть 1. Проблемы транзакций

Наиболее распространенная причина использования транзакций в приложениях - обеспечение высокого уровня целостности и непротиворечивости данных. Если вас не волнует качество данных, нет причин заботиться и о транзакциях. В конце концов, поддержка транзакций на платформе Java может убить производительность, породить проблемы с блокировками и целостностью данных в БД, а также усложнить приложение.

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

Невежество в области поддержки транзакций – еще один источник проблем. Слишком часто я слышу заявления вроде «нам не нужна поддержка транзакций в приложениях, потому что наши приложения безотказны». Отлично. Я видал приложения, которые на самом деле редко или никогда не выдавали исключений. Эти приложения основываются на хорошо написанном коде, хорошо написанных утилитах проверки и полном тестировании кода во избежание потерь производительности и усложнений, связанных с обработкой транзакций. Проблема с таким образом мыслей в том, что он берет в расчет только одну характеристику поддержки транзакций: атомарность. Атомарность гарантирует, что все изменения рассматриваются как единое целое и все вместе либо принимаются, либо откатываются. Но откат или координирование изменений – не единственный аспект поддержки транзакций. Другой аспект, изоляция, гарантирует, что одна единица работы изолирована от другой. Без надлежащей изоляции транзакций другие единицы работы могут обратиться к изменениям, произведенным еще незавершенной единицей работы. В результате бизнес-решения могут быть приняты на основе неполных данных, которые могут привести к срыву сделок или другим негативным (или дорогостоящим) последствиям.

ПРИМЕЧАНИЕ

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

Я понял размах проблем с обработкой транзакций в начале 2000-ых, когда, работая у клиента, заметил строку в плане проекта, прямо перед пунктом «Тестирование системы». Она гласила «реализовать поддержку транзакций». Конечно, несложно ведь добавить поддержку транзакций в систему, практически готовую к тестированию, не так ли? К сожалению, это очень широко распространенный подход. Но в этом проекте, в отличие от многих других, предусмотрена поддержка транзакций, хотя бы и в конце цикла разработки.

Таким образом, учитывая высокую стоимость и отрицательное влияние плохих данных, а также базовое представление о том, что транзакции важны (и нужны), вы должны использовать транзакции и научиться бороться с проблемами, которые могут возникнуть. Итак, вы добавляете в приложение поддержку транзакций. С этого-то часто и начинаются проблемы. На платформе Java транзакции далеко не всегда работают так, как предполагается. Эта статья объясняет причины этого. На примерах кода я покажу некоторые распространенные проблемы использования транзакций, которые я постоянно встречаю на практике, в рабочей среде.

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

Проблемы с локальными транзакциями

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

Листинг 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(), что приведет к противоречивости данных в БД. Если бы метод placeTrade() использовал транзакции, оба эти действия были бы включены в одну LUW, и заказ был бы отменен в случае неудачи обновления баланса счета.

Благодаря росту популярности таких фреймворков Ja­va-персистентности, как Hibernate, TopLink, Java Per­sis­tence API (JPA), мы теперь редко напрямую пишем JDBC-код. Чаще мы для упрощения жизни используем ORM-фреймворки, позволяющие заменить гнусный JDBC-код несколькими простыми вызовами методов. Например, чтобы вставить заказ из примера JDBC-кода в листинге 1, используя Spring Framework и JPA, можно отобразить объект TradeData на таблицу TRADE и заменить весь JDBC-код JPA-кодом из листинга 3:

Листинг 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() Entity­Manager-а, чтобы вставить заказ. Просто, правда? Не очень-то. Этот код не вставит заказ в таблицу TRADE, как ожидается, и не сгенерирует исключение. Он просто вернет значение 0 как ключ для заказа без изменения БД. Это одна из первых крупных засад в обработке транзакций: ORM-фреймворки нуждаются в транзакциях для запуска синхронизации между объектным кэшем и БД. Именно через транзакции генерируется SQL-код и выполняется нужное воздействие на БД (то есть вставка, удаление или изменение). Без транзакций ORM не запускается для генерирования SQL-кода и сохранения изменений, так что метод просто завершается – без исключений и без действия. Если вы используете ORM-Framework, вы должны использовать транзакции. Вы не можете больше полагаться на БД в управлении подключениями и фиксации изменений.

Эти простые примеры показывают, что транзакции необходимы для поддержки целостности и непротиворечивости данных. Но они только чуть-чуть касаются сложностей и проблем, связанных с реализацией транзакций на платформе 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 использовать аннотацию @Transaction при применении перехватчика транзакций. В его отсутствие аннотация @Transaction игнорируется, и в результате в коде никаких транзакций не используется.

Заставить работать аннотацию @Transaction в коде листинга 4 – это только начало. Заметьте, что Листинг 4 использует аннотацию @Transaction без указания каких-либо дополнительных параметров аннотации. Я обнаружил, что многие разработчики используют аннотацию @Transaction, не тратя времени на то, чтобы разобраться, что же она делает. Например, при таком использовании аннотации @Transaction как в листинге 4, какой устанавливается режим распространения транзакции? Каково состояние флага read-only? Каков уровень изоляции транзакций? Что более важно, когда транзакция должна откатить работу? Понимать, как используется эта аннотация, важно, чтобы обеспечить нужный уровень поддержки транзакций в вашем приложении. Вот ответы на только что заданные мной вопросы: при использовании аннотации @Transaction без параметров режим распространения будет установлен в REQUIRED, флаг read-only – в false, уровень изоляции транзакций – в READ_COMMITTED, и транзакция не будет откатываться в случае проверяемого исключения.

Проблемы с флагом read-only аннотации @Transactional

В своих странствиях я часто сталкивался с проблемой неверного использования флага read-only аннотации @Transactional. Вот вам краткий тест: что делает аннотация @Transactional в листинге 5 при использовании стандартного JDBC-кода, если флаг read-only установлен в true, и задан режим распространения транзакций SUPPORTS?

Листинг 5. Использование read-only с режимом распространения транзакций SUPPORTS – JDBC
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception 
{
   //JDBC Code...
}

При исполнении метода insertTrade() из листинга 5 он:

Генерирует исключение «read-only connection»?

Корректно вставляет заказ и сохраняет данные?

Ничего не делает, поскольку задан режим распространения транзакций SUPPORTS?

Сдаетесь? Правильный ответ – В. Заказ корректно вставляется в БД, даже если флаг read-only установлен в true и задан режим распространения транзакций SUPPORTS. Но как это может быть? Никакая транзакция не создается, из-за режима распространения SUP­PORTS, так что метод использует локальную транзакцию. Флаг read-only применяется только если транзакция началась. В данном случае она не началась, и флаг игнорируется.

ОК, если дело в этом, что будет делать аннотация @Transactional в листинге 6, если флаг read-only установлен, и задан режим распространения REQUIRED?

Листинг 6. Использование read-only с режимом распространения REQUIRED - JDBC
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception 
{
  //JDBC code...
}

Метод insertTrade():

Генерирует исключение «read-only connection»?

Корректно вставляет заказ и сохраняет данные?

Ничего не делает, поскольку флаг read-only установлен в true?

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

Странность флага read-only состоит в необходимости начала транзакции для его использования. Зачем нужна транзакция, если вы только читаете данные? Ответ – она вам не нужна. Создание транзакции для выполнения операций чтения создает излишнюю нагрузку на поток исполнения и может вызвать разделяемую блокировку чтения БД (в зависимости от используемого типа БД и уровня изоляции). Вывод – флаг read-only не имеет смысла при использовании JDBC Java-персистентности и создает лишнюю нагрузку, поскольку создает ненужные транзакции.

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

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

Метод insertTrade() в листинге 7:

Генерирует исключение «read-only connection»?

Корректно вставляет заказ и сохраняет данные?

Ничего не делает, поскольку флаг read-only установлен в true?

Правильный ответ – В. Заказ корректно вставляется в БД без ошибок. Минуточку – предыдущий пример показывает, что при использовании режима распространения REQUIRED будет сгенерировано исключение read-only-подключения. Это верно при использовании JDBC. При использовании ORM-фреймворка флаг read-only – это только подсказка для СУБД и директива для ORM-фреймворка (в данном случае Hibernate) установить режим NEVER очистки объектного кэша, указывающий, что кэш объектов не нужно синхронизировать с БД на протяжении этой единицы работы. Однако режим распространения REQUIRED все это переопределяет, позволяее транзакции начаться и рабтать так же, как в отсутствие флага read-only.

Это приводит к следующей часто встречающейся проблеме. Учитывая все, что вы уже прочли, предположите, что будет делать код листинга 8 если вы просто установите флаг read-only для аннотации @Tran­s­ac­tional?

Листинг 8. Использование read-only — JPA
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

Метод getTrade()в листинге 8:

Начинает транзакцию, получает заказ, фиксирует транзакцию?

Получает заказ без запуска транзакции?

Правильные ответ – А. Транзакция начинается и фиксируется. Не забудьте: режим распространения по умолчанию для аннотации @Transactional - REQUIRED. Это значит, что транзакция начинается даже когда она на само деле не нужна. В зависимости от используемой СУБД это может стать причиной ненужных разделяемых блокировок, что может привести к взаимоблокировкам. Кроме того, бессмысленно тратятся процессорное время и ресурсы на запуск и окончание транзакции. Вывод: при использовании ORM-фреймворка флаг read-only тоже бесполезен и в большинстве случаев игнорируется. Но если вы настаиваете на его использовании, всегда устанавливайте режим распространения SUPPORTS, как показано в Листинге 9, при этом никакая транзакция не запускается.

Листинг 9. Использование флага read-only и режима распространения 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 всегда начинает новую транзакцию при начале исполнения метода, независимо от наличия существующей транзакции. Многие разработчики неправильно используют атрибут 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 {...}

Обратите внимание, что оба метода – public, то есть они могут вызываться независимо друг от друга. Проблема с атрибутом REQUIRES_NEW возникает, когда использующие его методы вызываются в пределах одной логической единицы работы через межсервисное взаимодействие. Предположим, что в листинге 11 вы можете в некоторых случаях вызвать метод updateAcct() независимо от какого–либо другого метода, но есть также и случаи, когда метод updateAcct()вызывается из метода insertTrade(). Теперь, если происходит исключение после вызова метода updateAcct(), заказ будет откачен, но изменения счета будут зафиксированы в БД, как показано в листинге 12:

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

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

Из-за такого поведения транзакционный атрибут 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-подтверждение как часть процесса заказа. Если e-mail-сервер не работает, нужно сгенерировать какое-то проверяемое исключение SMTP, показывающее, что сообщение не может быть отправлено. Если бы проверяемое исключение вызывало автоматический откат, весь заказ книги был бы отменен только из-за того, что лежит почтовый сервер. Благодаря отсутствию автоматического отката для проверяемых транзакций вы можете перехватить это исключение и выполнить какие-либо корректирующие действия (например, поместить сообщение в очередь на отправку) и сохранить остальную часть заказа.

При использовании декларативной модели транзакций (подробно описанной во второй статье этой серии) вы можете указать, как контейнер или фреймворк должен обрабатывать проверяемые исключения. В Spring Fra­mework это указывается в параметре 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;
   }
}

Обратите внимание на использование параметра roll­backFor аннотации @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 связано немало проблем (включая не очень распространенные, которые я здесь не рассматривал). Большинство из них осложняется тем, что нет никаких предупреждений компиляторов или ошибок времени исполнения, говорящих о некорректной реализации транзакций. Более того, вопреки предположению, отраженному в приведенном выше анекдоте «лучше позже, чем никогда», реализация поддержки транзакций – это не только упражнение в кодировании. Существенные усилия при дизайне тратятся на разработку общей транзакционной стратегии. Следующие части этой статьи помогут вам в разработке эффективных транзакционных стратегий для различных случае использования – от простых приложений до высокопроизводительной обработки транзакций.

Часть 2. Обзор моделей и стратегий

Очень часто разработчики, дизайнеры и архитекторы путают модели транзакций с транзакционными стратегиями. На встречах с клиентами я обычно прошу архитектора или технического руководителя описать транзакционную стратегию их проекта. Как правило, я получаю один из трех ответов. Иногда это спокойное «Видите ли, мы не используем транзакции в наших приложениях». Порой я слышу смущенное «Я не уверен, что понимаю, что вы имеете в виду». Обычно же, однако, я получаю уверенный ответ «Мы используем декларативные транзакции». Но, как вы увидите в этой статье, термин «декларативные транзакции» описывает вовсе не транзакционную стратегию, а модель транзакций.

Платформа Java поддерживает три модели транзакций:

Эти модели описывают основы поведения транзакция на платформе Java и их реализацию. Однако они содержат только правила и семантику обработки транзакций. Как применять модель транзакций, остается на ваше усмотрение. Например, когда нужно использовать транзакционный атрибут REQUIRED, а когда – MANDATORY? Когда и где нужно указывать директивы отката транзакций? Когда использовать модель программных транзакций, а когда - декларативных? Как оптимизировать транзакции для высокопроизводительных систем? Сами по себе модели транзакций не могут ответить на эти вопросы. На них придется отвечать вам, либо рарабатывая собственную транзакционную стратегию, либо используя одну из четырех транзакционных стратегий, описанных ниже.

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

Модель локальных транзакций

Модель локальных транзакций называется так потому, что транзакциями управляет менеджер ресурсов нижележащей СУБД, а не контейнер или фреймворк, в котором исполняется ваше приложение. В этой модели вы управляете подключениями, а не транзакциями. Как говорилось в первой части статьи, эту модель нельзя использовать при внесении изменений в БД с использованием ORM-фреймворков типа Hibernate, TopLink или Java Persistence API (JPA). Эта модель применима при использовании DAO или JDBC-фреймворков и хранимых процедур СУБД.

Модель локальных транзакций можно применять одним из двух способов: позволить СУБД управлять подключениями или управлять ими программно. Чтобы разрешить СУБД управлять подключением, нужно установить значение свойства autoCommit JDBC-объекта Connection в true (используется по умолчанию). Это скажет нижележащей СУБД фиксировать транзакции после завершения операций вставки, удаления или изменения, или откатывать транзакции в случае неудачи этих операций. Эта техника показана в листинге 1, где заказ вставляется в таблицу TRADE:

Листинг 1. Локальные транзакции с одним изменением
public class TradingServiceImpl 
{
  public void processTrade(TradeData trade) throws Exception 
  {
    Connection dbConnection = null;
    try 
    {
      DataSource ds = (DataSource)
        (new InitialContext()).lookup("jdbc/MasterDS");
      dbConnection = ds.getConnection();
      dbConnection.setAutoCommit(true);
        Statement sql = dbConnection.createStatement();
        String stmt = "insert into TRADE ...";
        sql.executeUpdate(stmt1);
    } 
    finally 
    {
      if (dbConnection != null)
        dbConnection.close();
    }
  }
}

В этом листинге значение свойства autoCommit равно true, указывая СУБД, что локальная транзакция должна быть зафиксирована после каждого выражения. Эта техника прекрасно работает в случае одиночного взаимодействия с БД в пределах одной логической единицы работы (logical unit of work, LUW). Предположим, однако, что метод processTrade(), приведенный в листинге 1, также обновляет баланс в таблице ACCT, отражая стоимость заказа. В этом случае два действия над БД будут независимы друг от друга, и вставка в таблицу TRADE будет зафиксирована в БД раньше, чем изменение таблицы ACCT. В случае сбоя при изменении таблицы ACCT, не будет никакого механизма отката вставки в таблицу TRADE, что приведет к несогласованности данных в БД.

Этот сценарий приводит ко второму способу: программному управлению подключениями. При этом значение свойства autoCommit JDBC-объекта Connection нужно установить в false и вручную фиксировать или откатывать подключения. В листинге 2 показан пример использования этой техники:

Листинг 2. Локальные транзакции со множественными изменениями
public class TradingServiceImpl 
{
  public void processTrade(TradeData trade) throws Exception 
  {
    Connection dbConnection = null;
    try 
    {
      DataSource ds = (DataSource)
        (new InitialContext()).lookup("jdbc/MasterDS");
      dbConnection = ds.getConnection();
      dbConnection.setAutoCommit(false);
        Statement sql = dbConnection.createStatement();
        String stmt1 = "insert into TRADE ...";
        sql.executeUpdate(stmt1);
        String stmt2 = "update ACCT set balance...";
        sql.executeUpdate(stmt2);
        dbConnection.commit();
    } 
    catch (Exception up) 
    {
      dbConnection.rollback();
      throw up;
    } 
    finally 
    {
      if (dbConnection != null)
        dbConnection.close();
    }
  }
}

В этом листинге значение свойства autoCommit равно false, указывая нижележащей СУБД, что подключениями будет управлять код, а не СУБД. В данном случае нужно вызывать метод commit() объекта Connection, если все в порядке, и метод rollback() в случае возникновения исключения. Таким образом, вы можете координировать два действия над БД в одной единице работы.

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

Модель программных транзакций

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

Аналогично примеру из листинга 2, в этой модели разработчик отвечает за получение транзакции у менеджера транзакций, начало транзакции, фиксацию транзакции и – в случае исключения – за откат транзакции. Как вы можете, скорее всего, предположить, это выливается в большое количество чреватого ошибками кода, путающегося под ногами у бизнес-логики приложения. Тем не менее, некоторые транзакционные стратегии требуют использования модели программных транзакций.

Несмотря на совпадение концепций, реализации модели программных транзакций различаются в Spring Framework и в спецификации EJB 3.0. Сперва я покажу реализацию этой модели с использованием EJB 3.0, а затем – то же самое, но с использованием Spring Framework.

Программные транзакции с использованием EJB 3.0

В EJB 3.0 вы получаете транзакцию у менеджера транзакций (другими словами, у контейнера) с помощью Java Naming and Directory Interface (JNDI) lookup в javax.transaction.UserTransaction. Получив UserTransaction, можно вызвать метод begin(), чтобы начать транзакцию, метод commit() для фиксации транзакции и метод rollback() для отката транзакции в случае ошибок. В этой модели контейнер не будет автоматически фиксировать или откатывать транзакцию; на совести разработчика запрограммировать это поведение в Java-методе, выполняющем изменения в БД. В листинге 3 показан пример реализации модели программных транзакций для EJB 3.0 c использованием JPA.

Листинг 3. Программные транзакции в EJB 3.0
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class TradingServiceImpl implements TradingService 
{
  @PersistenceContext(unitName="trading") EntityManager em;

  public void processTrade(TradeData trade) throws Exception 
  {
    InitialContext ctx = new InitialContext();
    UserTransaction txn = (UserTransaction)ctx.lookup("UserTransaction");
    try 
    {
      txn.begin();
        em.persist(trade);
        AcctData acct = em.find(AcctData.class, trade.getAcctId());
        double tradeValue = trade.getPrice() * trade.getShares();
        double currentBalance = acct.getBalance();
      if (trade.getAction().equals("BUY")) 
      {
        acct.setBalance(currentBalance - tradeValue);
      } 
      else 
      {
        acct.setBalance(currentBalance + tradeValue);
      }
      txn.commit();
    } 
    catch (Exception up) 
    {
      txn.rollback();
      throw up;
    }
  }
}

При использовании модели программных транзакций в среде контейнера Java EE со stateless сессионным бином, вы должны сказать контейнеру, что используете программные транзакции. Это делается с помощью аннотации @TransactionManagement и задания типа транзакции BEAN. Если не использовать эту аннотацию, контейнер будет считать, что вы используете декларативное управление транзакциями (CONTAINER), тип транзакций, используемый в EJB 3.0 по умолчанию. При использовании программных транзакций в клиентском слое вне контекста stateless сессионного бина задавать тип транзакции не нужно.

Программные транзакции в Spring

В Spring Framework есть два способа реализации модели программных транзакций. Один способ – через Spring TransactionTemplate, другой – с использованием менеджера транзакций Spring напрямую. Поскольку я не большой любитель анонимных внутренних классов и трудночитаемого кода, я буду использовать второй способ.

В Spring есть как минимум девять менеджеров транзакций. Самые распространенные, которые вы и будете, скорее всего, использовать, это DataSource­Trans­ac­ti­on­Ma­nager, HibernateTransaction­Manager, JpaTransaction­Ma­nager и JtaTransactionManager. В моих примерах кода используется JPA, так что я покажу конфигурацию для JpaTransactionManager.

Чтобы сконфигурировать JpaTransactionManager в Spring, просто определите бин в XML-файле контекста приложения, используя класс org.springframe­work.­orm.­jpa.JpaTransactionManager, и добавьте ссылку в бин JPA Entity Manager Factory. Затем, считая, что класс, содержащий логику приложения, управляется Spring, введите в бин менеджер транзакций, как показано в листинге 4.

Листинг 4. Определение менеджера транзакций JPA Spring
<bean id="transactionManager"
      class="org.springframework.orm.jpa.JpaTransactionManager">
  <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<bean id="tradingService" class="com.trading.service.TradingServiceImpl">
 <property name="txnManager" ref="transactionManager"/>
</bean>

Если Spring не управляет классом приложения, вы можете получить ссылку на менеджер транзакций в вашем методе, используя метод getBean() для контекста Spring.

Теперь в исходном коде можно использовать менеджер транзакций платформы для получения транзакции. Когда все изменения внесены, можно вызвать метод commit() для фиксации транзакции или метод rollback() для ее отката, как показано в листинге 5.

Листинг 5. Использование менеджера транзакций JPA Spring
public class TradingServiceImpl  
{
  @PersistenceContext(unitName="trading") EntityManager em;

  JpaTransactionManager txnManager = null;
  public void setTxnManager(JpaTransactionManager mgr) 
  {
    txnManager = mgr;
  }

  public void processTrade(TradeData trade) throws Exception 
  {
    TransactionStatus status =
      txnManager.getTransaction(new DefaultTransactionDefinition());
    try 
    {
      em.persist(trade);
      AcctData acct = em.find(AcctData.class, trade.getAcctId());
      double tradeValue = trade.getPrice() * trade.getShares();
      double currentBalance = acct.getBalance();
      if (trade.getAction().equals("BUY")) 
      {
        acct.setBalance(currentBalance - tradeValue);
      } 
      else 
      {
        acct.setBalance(currentBalance + tradeValue);
      }
      txnManager.commit(status);
    } 
    catch (Exception up) 
    {
      txnManager.rollback(status);
      throw up;
    }
  }
}

Обратите внимание на различия между Spring Fra­mework и EJB 3.0. В Spring транзакция получается (и затем запускается) вызовом метода getTransaction() у менеджера транзакций платформы. Анонимный класс DefaultTransactionDefinition содержит детали транзакции и ее поведения, включая имя транзакции, уровень изоляции, режим распространения (транзакционный атрибут) и срок таймаута транзакции (если он задан). В данном случае я просто использую значения по умолчанию, то есть пустую строку в качестве имени, уровень изоляции по умолчанию для нижележащей СУБД (RE­AD_COMMITTED), транзакционный атрибут PROPA­GATION_REQUIRED и таймаут СУБД, используемый по умолчанию. Заметьте также, что методы commit() and rollback() вызываются с использованием менеджера транзакций, а не транзакции (как в случае EJB).

Модель декларативных транзакций

Модель декларативных транзакций, известная также как Container Managed Transactions (CMT, транзакции, управляемые контейнером) – это наиболее распространенная транзакционная модель на платформе Java. В этой модели среда контейнера заботится о запуске, фиксации и окате транзакций. Разработчик отвечает только за указание поведения транзакции. Большинство проблем с транзакциями, обсуждавшихся в первой части статьи, относятся именно к модели декларативных транзакций.

И Spring Framework, и EJB 3.0 для указания поведения транзакций используют аннотации. В Spring используется аннотация @Transactional, а в EJB 3.0 – аннотация @TransactionAttribute. При использовании модели декларативных транзакций контейнер не будет автоматически откатывать транзакцию в случае проверяемого исключения. Разработчик должен указывать, где и когда откатывать транзакции в случае проверяемого исключения. В Spring Framework это указывается с помощью свойства rollbackFor аннотации @Transactional. В EJB это делается вызовом метода setRollbackOnly() у Ses­sionContext.

В листинге 6 показано использование модели декларативных транзакций в EJB.

Листинг 6. Декларативные транзакции в EJB
@Stateless
public class TradingServiceImpl implements TradingService 
{
  @PersistenceContext(unitName="trading") EntityManager em;
  @Resource SessionContext ctx;

  @TransactionAttribute(TransactionAttributeType.REQUIRED)
  public void processTrade(TradeData trade) throws Exception 
  {
    try 
    {
      em.persist(trade);
      AcctData acct = em.find(AcctData.class, trade.getAcctId());
      double tradeValue = trade.getPrice() * trade.getShares();
      double currentBalance = acct.getBalance();
      if (trade.getAction().equals("BUY")) 
      {
        acct.setBalance(currentBalance - tradeValue);
      } 
      else 
      {
        acct.setBalance(currentBalance + tradeValue);
      }
    } 
    catch (Exception up) 
    {
      ctx.setRollbackOnly();
      throw up;
    }
  }
}

В листинге 7 показано использование модели декларативных транзакций в Spring Framework.

Листинг 7. Декларативные транзакции в Spring
public class TradingServiceImpl 
{
  @PersistenceContext(unitName="trading") EntityManager em;

  @Transactional(propagation=Propagation.REQUIRED,
    rollbackFor=Exception.class)
  public void processTrade(TradeData trade) throws Exception 
  {
    em.persist(trade);
    AcctData acct = em.find(AcctData.class, trade.getAcctId());
    double tradeValue = trade.getPrice() * trade.getShares();
    double currentBalance = acct.getBalance();
    if (trade.getAction().equals("BUY")) 
    {
      acct.setBalance(currentBalance - tradeValue);
    } 
    else 
    {
      acct.setBalance(currentBalance + tradeValue);
    }
  }
}

Транзакционные атрибуты

Кроме директив отката, нужно указать также транзакционный атрибут, который определяет поведение транзакции. Платформа Java поддерживает шесть типов транзакционных атрибутов, независимо от того, используете вы EJB или Spring Framework:

Описывая каждый из них, я буду использовать фиктивный метод methodA(), к которому и будет применяться транзакционный атрибут.

Если для метода methodA() указан транзакционный атрибут Required, и methodA() вызывается из существующей транзакции, будет использоваться область видимости существующей транзакции. В противном случае methodA() начнет новую транзакцию. Если транзакцию начал methodA(), то и завершать ее (фиксировать или откатывать) должен тоже methodA(). Это чаще всего применяемый транзакционный атрибут, используемый по умолчанию как в EJB 3.0, так и в Spring. К сожалению, во многих случаях он используется некорректно, что приводит к проблемам с целостностью и непротиворечивостью данных. Для каждой из транзакционных стратегий я в следующих статьях серии более подробно рассмотрю использование этого атрибута.

Если для метода methodA() указан транзакционный атрибут Mandatory, и methodA() вызывается из области видимости существующей транзакции, будет использоваться область видимости существующей транзакции. Однако если methodA() вызывается вне контекста транзакции, будет сгенерировано исключение Trans­ac­ti­on­RequiredException, указывающее, что транзакция уже должна существовать до вызова methodA(). Этот транзакционный атрибут используется в транзакционной стратегии Client Orchestration, описанной в следующем разделе этой статьи.

Интересен транзакционный атрибут RequiresNew. Я встречаюсь с неверным использованием или пониманием этого атрибута чаще, чем с верным. Если для methodA() указан транзакционный атрибут RequiresNew, и methodA() вызывается изнутри или извне контекста транзакции, methodA() всегда начинает (и заканчивает) новую транзакцию. Это значит, что если methodA() вызывается из контекста другой транзакции (например, Transaction1), Transaction1 будет приостановлена, и будет начата новая транзакция (Transaction2). После завершения methodA()Transaction2 будет зафиксирована или отменена, и возобновится исполнение Transaction1. Это, очевидно, нарушает свойства транзакций ACID (ACID – атомарность, непротиворечивость, изоляция, надежность), особенно атомарность. Другими словами, все обновления БД в этом случае уже не содержатся в одной единице работы. В случае отката Transaction1 изменения, внесенные Trans­action2, сохранятся. В таком случае, зачем нужен такой транзакционный атрибут? Как было показано в первой статье серии, этот транзакционный атрибут нужно использовать только при операциях над БД (например, аудите или логировании), независимых от нижележащих транзакций (в данном случае Transaction1).

Транзакционный атрибут Supports – еще один, который, я считаю, большинство разработчиков не совсем понимает или оценивает. Если для methodA() указан транзакционный атрибут Supports, и methodA() вызывается в контексте существующей транзакции, methodA() будет исполняться в контексте существующей транзакции. Если же methodA() вызывается вне контекста транзакции, новая транзакция не начинается. Этот атрибут в основном используется при операциях чтения из БД. Почему бы в этом случае не использовать транзакционный атрибут NotSupported (описанный в следующем абзаце)? В конце концов, этот атрибут гарантирует, что метод будет выполняться без использования транзакций. Ответ прост. Исполнение запроса в контексте существующей транзакции приведет к считыванию данных из лога транзакций БД (проще говоря, измененных данных), а при его исполнении вне контекста транзакции будут считаны неизмененные данные из таблицы. Например, если вы хотите вставить в таблицу Trade новый заказ и затем (в той же транзакции) получить список заказов, незафиксированный заказ будет присутствовать в списке. Но если бы вы использовали транзакционный атрибут NotSupported, запрос обращался бы к таблице, а не к логу транзакций. Таким образом, вы не увидели бы в списке незафиксированного заказа. Это не обязательно плохо, все зависит от бизнес-логики и конкретной ситуации.

Транзакционный атрибут NotSupported указывает, что вызываемый метод не будет использовать или начинать транзакцию, независимо от наличия таковой. Если для methodA() указан транзакционный атрибут NotSupported, и methodA() вызывается в контексте существующей транзакции, транзакция приостанавливается до окончания выполнения метода. После окончания выполнения метода исходная транзакции возобновляется. Есть всего несколько случаев использования этого транзакционного атрибута, и в основном они относятся к использованию хранимых процедур. Если вы пытаетесь вызвать хранимую процедуру из контекста существующей транзакции, и хранимая процедура содержит BEGIN TRANS или, в случае Sybase, исполняется в unchained-режиме, будет выдано исключение, указывающее, что новая транзакция не может быть начата в случае наличия уже существующей (проще говоря, что вложенные транзакции не поддерживаются). Почти все контейнеры используют Java Transaction Service (JTS) как реализацию транзакций для JTA по умолчанию. Это именно JTS – а не платформа Java как таковая – не поддерживает вложенных транзакций. Если нельзя изменить хранимую процедуру, можно использовать атрибут NotSupported, чтобы прервать существующую транзакцию и избежать фатального исключения. При этом, конечно, вы уже не сможете внести атомарные изменения в БД в той же LUW. Это компромисс, но он может быстро вытащить вас из затруднительной ситуации.

Транзакционный атрибут Never, возможно, самый интересный из всех. Он работает так же, как NotSupported, но с одной существенной разницей. Если при вызове метода с транзакционным атрибутом Never уже существует контекст транзакции, генерируется исключение, указывающее, что при вызове этого метода транзакции недопустимы. Единственный случай использования этого атрибута, который я смог придумать – тестирование. Он предоставляет простой и быстрый способ проверки наличия транзакции на момент вызова конкретного метода. Если вы используете транзакционный атрибут Never и получаете исключение при вызове проверяемого метода, значит, транзакция существует, и наоборот. Это хороший способ проверить, что ваша транзакционная стратегия работает.

Транзакционные стратегии

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

Здесь я коротко опишу каждую из этих стратегий. Подробно они будут рассмотрены в следующих статьях серии.

Транзакционная стратегия Client Orchestration используется, когда несколько вызовов из клиентского слоя выполняют одну единицу работу. Клиентом в данном случае может выступать Web-фреймворк, портальное приложение, настольный компьютер или, в некоторых случаях, система документооборота или BPM-ком­по­нент. В сущности, клиент распоряжается процессом вычислений и «шагами», необходимыми для выполнения конкретного запроса. Например, чтобы разместить заказ, нужно внести его в БД и обновить баланс счета заказчика, чтобы отразить стоимость заказа. Если слой API приложения слишком тонко гранулирован, придется вызывать оба метода из клиентского слоя. В данном сценарии транзакционная единица работы должна находиться в клиентском слое, чтобы обеспечивать атомарную единицу работы.

Транзакционная стратегия API Layer используется в случае грубо гранулированных методов, работающих как точки доступа к back-end-функциональности (если вам хочется, называйте ее сервисами). В этом сценарии клиенты (будь они Web-клиентами, Web-сервисами, основанными на обмене сообщениями или даже настольными приложениями) для выполнения конкретного запроса производят один вызов сервисов. По сценарию с заказом из предыдущего абзаца в данном случае у вас будет один метод (например, processTrade()), к которому будет обращаться клиентский слой. Этот единственный метод должен содержать все необходимое для размещения заказа и обновления баланса счета. Я так назвал эту стратегию потому, что в большинстве случаев серверная функциональность предоставляется клиентским приложениям с помощью использования интерфейсов или API. Это одна из наиболее распространенных транзакционных стратегий.

Транзакционная стратегия высокого параллелизма – это вариант транзакционной стратегии API Layer, который используется для приложений, неспособных поддерживать длительные (long-running, т.е. фактически, бизнес-транзакции) транзакции из слоя API (обычно по причинам производительности или масштабируемости). Как следует из названия, эта стратегия используется в основном в приложениях, обеспечивающих высокий уровень параллелизма для пользователя. Транзакции на платформе Java весьма дороги. В зависимости от используемой СУБД, они могут вызывать блокировки в БД, занимать ресурсы, замедлять работу приложения и даже, в некоторых случаях, приводить к взаимоблокировкам. Основная идея этой транзакционной стратегии состоит в максимальном сокращении охвата транзакции, чтобы минимизировать количество блокировок при одновременной поддержке атомарности работы для любого клиентского запроса. В некоторых случаях для поддержки этой транзакционной стратегии может потребоваться рефакторинг логики приложения.

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

Заключение

Как видно из этого обзора, разработка эффективной транзакционной стратегии – это не всегда простая задача. Решение проблемы целостности и непротиворечивости данных вовлекает в себя множество предположений, возможностей, моделей, фреймворков и методик. За много лет работы над приложениями и с транзакциями я обнаружил, что хотя число опций, настроек, моделей и конфигураций может казаться не умещающимся в голове, в действительности только несколько комбинаций различных возможностей имеют смысл в большинстве случаев. Четыре разработанных мною транзакционных стратегии, которые будут подробно разобраны в следующих статьях этой серии, должны покрывать большинство сценариев, используемых при разработке бизнес-приложений на платформе Java. Краткое предостережение. Эти стратегии – не «серебряная пуля». Иногда для их реализации требуется рефакторинг кода или переработка дизайна приложения. В таких ситуациях вам просто нужно спросить себя, насколько важны целостность и непротиворечивость ваших данных? В большинстве случаев усилия, затрачиваемые на рефакторинг, просто несоизмеримы с риском возможных потерь.

Ресурсы


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

Copyright © 1994-2016 ООО "К-Пресс"