Что такое транзакция java
Перейти к содержимому

Что такое транзакция java

  • автор:

Что такое транзакция java

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

Java Transaction API (JTA) позволяет приложениям получать доступ к транзакциям способом, который не зависит от конкретных реализаций. JTA определяет стандартные интерфейсы Java между менеджером транзакций и сторонами, участвующими в системе распределённых транзакций: транзакционным приложением, сервером Java EE и менеджером, который контролирует доступ к общим ресурсам, на которые влияют транзакции.

JTA определяет интерфейс UserTransaction , который приложения используют для запуска, фиксации или отката транзакций. Компоненты приложения получают объект UserTransaction через поиск JNDI, используя имя java:comp/UserTransaction или инъецируя объект UserTransaction . Сервер приложений использует несколько JTA-определённых интерфейсов для связи с менеджером транзакций. Менеджер транзакций использует JTA-определённые интерфейсы для взаимодействия с менеджером ресурсов.

Управление транзакциями, commit rollback

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

По умолчанию каждое новое соединение находится в режиме автофиксации (autocommit = true). Это означает автоматическую фиксацию (commit) транзакции после выполнения каждого запроса. В этом случае транзакция включает только одно изменение (один запрос).

Если autocommit запрещен, т.е. равен false, то транзакция не заканчивается до явного вызова commit или rollback, включая, таким образом, все выражения, выполненные с момента последнего вызова commit или rollback. В этом случае все SQL-запросы в транзакции фиксируются или откатываются группой.

Метод фиксации commit завершает все изменения в БД, проделанные SQL-выражением, и снимает также все блокировки, установленные транзакцией. Метод rollback наоборот — не сохранит изменения и восстановит исходное состояние на момент начала транзакции.

Иногда пользователю нужно, чтобы какое-либо изменение не вступило в силу до тех пор, пока не вступит в силу предыдущее изменение. Этого можно достичь запрещением autocommit и группировкой обоих запросов в одну транзакцию. Если оба изменения произошли успешно, то вызывается метод commit, который переносит эффект от этих изменений в БД; если одно или оба запроса не прошли, то вызывается метод rollback, который возвращает прежнее состояние БД.

Большинство JDBC-драйверов поддерживают транзакции. В действительности драйвер, соответствующий спецификации JDBC, обязан поддерживать их. Интерфейс DatabaseMetaData позволяет получить информацию об уровнях изолированности транзакций, которые поддерживаются данной СУБД.

Пример использования транзакции — commit, autocommit
Connection connection = . ; // Сброс автофиксации connection.setAutoCommit(false); // Первая транзакция PreparedStatement updateSales = connection.prepareStatement( "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?"); updateSales.setInt(1, 50); updateSales.setString(2, "Colombian"); updateSales.executeUpdate(); // Вторая транзакция PreparedStatement updateTotal = connection.prepareStatement( "UPDATE COFFEES SET TOTAL = TOTAL + ? WHERE COF_NAME LIKE ?"); updateTotal.setInt(1, 50); updateTotal.setString(2, "Colombian"); updateTotal.executeUpdate(); // Завершение транзакции connection.commit(); // Восстановление по умолчанию connection.setAutoCommit(true);

В примере для соединения Connection режим автофиксации отключен и два оператора updateSales и updateTotal будут зафиксированы вместе при вызове метода commit.

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

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

Уровни изолированности транзакций, dirty read

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

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

Есть несколько способов разрешения конфликтов между одновременно выполняющимися транзакциями. Разработчик может определить уровень изолированности так, что пока одна транзакция изменяет какое-либо значение, вторая транзакция могла бы прочитать обновленное значение до того, пока первая не выполнит commit или rollback. Для этого следует установить уровень изолированности TRANSACTION_READ_UNCOMMITTED:

Connection connection; . connection.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);

В данном коде серверу указано на возможность чтения измененных значений до того, как выполнится commit, т.е. определена возможность «грязного чтения» («dirty read«).

По умолчанию уровень изоляции транзакций обычно установлен в READ_COMMITED.

Изменение уровня изолированности во время транзакции нежелательно, так как произойдет автоматический вызов commit, что повлечет за собой фиксацию изменений.

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

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

Обычно, чем выше уровень изолированности, тем медленнее выполняется приложение (из-за избыточной блокировки). При выборе конкретного уровня изолированности разработчик должен найти золотую середину между потребностями в производительности и требованиями к целостности данных. Очевидно, что реально поддерживаемые уровни зависят от возможностей используемой СУБД.

При создании объекта Connection уровень его изолированности зависит от драйвера или БД. Можно вызвать метод setIsolationLevel, чтобы изменить уровень изолированности транзакций, и новое значение уровня будет установлено до конца сессии. Чтобы установить уровень изолированности только для одной транзакции, надо установить его перед выполнением транзакции и восстановить прежнее значение после ее завершения.

Типы уровней изолированности
  • TRANSACTION_NONE
    Транзакции не поддерживаются.
  • TRANSACTION_READ_COMMITTED
    Запрет на «грязное чтение» (dirty read). Данный уровень блокирует транзакциям чтение строк с неподтвержденными изменениями в них.
  • TRANSACTION_READ_UNCOMMITTED
    Разрешение на «dirty read». Данный уровень позволяет изменять строку с помощью одной транзакции и прочесть ее другой прежде, чем изменения в этой строке будут подтверждены (dirty read). Если изменения будут отменены с помощью rollback(), вторая транзакция вернет неправильную строку.
  • TRANSACTION_REPEATABLE_READ
    Запрет на «dirty read». Данный уровень препятствует транзакции от чтения строки с неподтвержденным изменением в ней, он также предотвращает ситуацию, когда одна транзакция читает строку, а вторая транзакция изменяет ее, при этом первая транзакция перечитывая строку, получает разные значения каждый раз (разовое чтение).
  • TRANSACTION_SERIALIZABLE
    Запрет на «dirty read». Данный уровень включает предотвращения из TRANSACTION_REPEATABLE_READ, более того предотвращает ситуацию, когда одна транзакция читает все строки, которые удовлетворяют условию WHERE, а вторая транзакция вставляет строку, которая удовлетворяет тому же условию WHERE, и первая транзакция, перечитывая с тем же условием, получает дополнительную «фантомную» строку при втором чтении.

Основы транзакций в Spring и JDBC

В этой статье мы разберемся, что такое транзакции. Какими обладают транзакции — ACID. Как транзакции выполняются на уровне JDBС, а также на уровне Spring.

12 окт. 2022 · 7 минуты на чтение

Мы начнём изучение транзакций с нуля, шаг за шагом погружаясь в тему. Быстро рассмотрим проблему, которую решают транзакции. Далее посмотрим, как написать транзакцию на SQL. А потом разберёмся с управлением транзакциями в JDBC. Всё, что делает Spring, основано на JDBC. И вы сэкономите кучу времени при работе с аннотацией @Transactional , если усвоите эти основы.

Спонсор поста
Переведенная статья

Данная статья является переводом и адаптацией англоязычной статьи. Я тщательно перевожу статью на русский, актуализирую устаревшие моменты, а также проверяю или пишу примеры кода, которые вы можете запустить самостоятельно.
– – – –
Spring Transaction Management: @Transactional In-Depth

Проблематика

Что такое транзакция и зачем она нужна проще объяснить на примере. Допустим, Вася переводит Пете 100 рублей. Для выполнения этой бизнес-функции нам потребуется три действия:

  • Списать деньги с баланса Васи;
  • Записать операцию перевода от Васи к Пете;
  • Добавить денег на баланс Пете;

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

Для решения таких проблем и существуют транзакции. Транзакция позволит нам объединить эти операции таким образом, что по итогу мы либо запишем все изменения, либо ничего. То есть объединить несколько различных действий в атомарную операцию.

Свойства транзакций ACID

Транзанкции обладют свойствами, которые называют ACID:

Атомарность (Atomicity) гарантирует, что никакая транзакция не будет зафиксирована в системе частично. Будут либо выполнены все операции, либо ни одной.

Согласованность (Consistency). Выполненая транзакция, сохраняет согласованность базы данных. Согласованность является более широким понятием, чем может показаться.

Например, в банковской системе может существовать требование равенства суммы, списываемой с одного счёта, сумме, зачисляемой на другой. Это бизнес-правило и оно не может быть гарантировано только проверками целостности базы данных. Это поведение должны учитывать программисты при написании кода транзакций. Если какая-либо транзакция произведёт списание, но не произведёт зачисления, то система останется в некорректном состоянии и свойство согласованности будет нарушено.

Изолированность (Isolation). Во время выполнения транзакции параллельные транзакции не должны оказывать влияние на её результат.

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

Устойчивость (Durability). Независимо от проблем с оборудованием, изменения, сделанные успешно завершённой транзакцией, должны остаться сохранёнными после возвращения системы в работу.

Транзакции в SQL

Коротко рассмотрим, как сделать транзакцию в SQL. Для этого используют ключевые слова BEGIN , COMMIT , ROLLBACK .

Возвращаясь к примеру с переводами, представим, что у нас есть 2 таблицы:

И мы хотим сделать перевод между первым и вторым пользователем 2. Других пользователей у нас в системе нет.

UPDATE person SET balance = (balance - 10) WHERE INTO transaction(person_from, person_to, amount) values (1, 3, 10); UPDATE person SET balance = (balance + 10) WHERE >Мы уменьшаем баланс первого пользователя, потом создаём запись в таблице переводов, но случайно ошибаемся и записываем перевод несуществующему третьему пользователю. После чего пытаемся обновить баланс второго пользователя.

Между таблицами transaction и person есть связь, а пользователя с идентификатором 3 не существует, поэтому мы не сможем выполнить INSERT . И дальнейшее выполнение кода будет остановлено, то есть у первого пользователя баланс уменьшается, а у второго не увеличивается.

Исправим эту ситуацию с помощью транзакции:

BEGIN; UPDATE person SET balance = (balance - 10) WHERE INTO transaction(person_from, person_to, amount) values (1, 3, 10); UPDATE person SET balance = (balance + 10) WHERE >Мы обернули код в команды BEGIN и COMMIT . Но теперь, когда произойдёт ошибка в операторе INSERT , изменения, внесенные первым оператором UPDATE , не будут сохранены в базу данных. Таким образом, все три изменения будут записаны либо вместе, либо не будут записаны вовсе. А теперь переходим к родному для нас JDBC.

Учтите, что после ошибки в транзакции необходимо вызвать оператор ROLLBACK . Для упрощения в примере этот момент опущен.

Управление транзакциями в JDBC

Первое, что вам стоит понять и запомнить: не имеет значения, используете ли вы аннотацию @Transactional от Spring, Hibernate, jOOQ или любую другую библиотеку для работы с базой данных. В конечном счёте все они делают одно и то же — открывают и закрывают транзакции базы данных.

Обычный код управления транзакциями JDBC выглядит следующим образом:

import java.sql.Connection; Connection connection = dataSource.getConnection(); // (1) try (connection) < connection.setAutoCommit(false); // (2) // execute some SQL statements. connection.commit(); // (3) >catch (SQLException e) < connection.rollback(); // (4) >
  1. Для запуска транзакций вам необходимо подключение к базе данных. DriverManager.getConnection(url, user, password) тоже подойдёт, хотя в большинстве корпоративных приложений вы будете иметь настроенный источник данных и получать соединения из этого источника.
  2. Это единственный способ начать транзакцию базы данных в Java, возможно, звучит немного странно. Вызов setAutoCommit(true) гарантирует, что каждый SQL-оператор будет автоматически завёрнут в собственную транзакцию, а setAutoCommit(false) — наоборот. Теперь вы должны управлять жизнью транзакции. Обратите внимание, что флаг autoCommit действует в течение всего времени, пока соединение открыто.
  3. Фиксируем транзакцию
  4. Или откатываем изменения, если возникло исключение.

Управление транзакциями в Spring

Поскольку теперь у вас есть понимание транзакций JDBC, посмотрим, как Spring управляет транзакциями. Это также применимо и к Spring Boot и Spring MVC.

В обычном JDBC у вас есть один способ ( setAutocommit(false) ) управлять транзакциями, Spring предлагает вам множество различных, более удобных способов добиться того же самого.

Программное управление транзакциями Spring

Первый, но довольно редко используемый способ внедрения транзакций – программный. Либо через TransactionTemplate , либо через PlatformTransactionManager . Это выглядит следующим образом:

@Service public class UserService < @Autowired private TransactionTemplate template; public Long registerUser(User user) < Long ->< // execute some SQL that e.g. // inserts the user into the db and returns the autogenerated id return id; >); > >

По сравнению с обычным примером JDBC:

  • Вам не нужно открывать и закрывать соединений с базой данных самостоятельно. Вместо этого, вы используете Transaction Callbacks.
  • Также не нужно ловить SQLExceptions , так как Spring преобразует их в исключения времени выполнения.
  • Кроме того, вы лучше интегрируетесь в экосистему Spring. TransactionTemplate будет использовать TransactionManager внутри, который будет использовать источник данных. Всё это бины, которые выукажете в конфигурации, но о которых вам больше не придется беспокоиться в дальнейшем.

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

Декларативное управление транзанкциями

Посмотрим, как обычно выглядит управление транзакциями в Spring:

public class UserService < @Transactional public Long registerUser(User user) < // execute some SQL that e.g. // inserts the user into the db and retrieves the autogenerated id // userDao.save(user); return id; >>

Никакого лишнего «технического» кода. Вместо этого, нужно сделать две вещи:

  • Убедиться, что одна из Spring конфигурации аннотирована @EnableTransactionManagement . В SpringBoot даже этого делать не нужно.
  • Убедиться, что вы указали менеджер транзакций в конфигурации. Это делается в любом случае.

И тогда Spring будет обрабатывать транзакции за вас. Любой публичный метод бина, который вы аннотируете @Transactional , будет выполняться внутри транзакции базы данных.

Итак, чтобы аннотация @Transactional заработала, делаем следующее:

@Configuration @EnableTransactionManagement public class MySpringConfig < @Bean public PlatformTransactionManager txManager() < return yourTxManager; // more on that later >>

Вооружившись знаниями из примера про транзакций JDBC, приведенный выше код можно упрощенно представить следующим образом:

Как видно из этой схемы, у прокси следующие задачи:

  • открытие и закрытие соединений/транзакций с базой данных;
  • А затем делегирование выполнения настоящему UserService , тому, который вы написали;
  • А другие бины, такие как ваш UserRestController , никогда не узнают, что они работают с прокси, а не с настоящим сервисом;

Для чего нужен менеджер транзакций?

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

Ваш UserService проксируется во время выполнения, и прокси управляет транзакциями. Но не сам прокси управляет всем транзакционным состоянием, прокси делегирует работу менеджеру транзакций.

Spring предлагает вам интерфейс PlatformTransactionManager / TransactionManager , который по умолчанию поставляется с парой удобных реализаций. Одна из них – DataSourceTransactionManager .

Он делает то же самое, что вы делали до сих пор для управления транзакциями, но сначала рассмотрим необходимую конфигурацию Spring:

@Bean public DataSource dataSource() < return new MysqlDataSource(); // (1) >@Bean public PlatformTransactionManager txManager() < return new DataSourceTransactionManager(dataSource()); // (2) >
  1. Создаем источник базы данных. В этом примере используется MySQL.
  2. Создаем менеджер транзакций, которому нужен источник данных, чтобы иметь возможность управлять транзакциями.

Все менеджеры транзакций имеют такие методы, как doBegin() или doCommit() , которые выглядят следующим образом:

public class DataSourceTransactionManager implements PlatformTransactionManager < @Override protected void doBegin(Object transaction, TransactionDefinition definition) < Connection newCon = obtainDataSource().getConnection(); // . con.setAutoCommit(false); // yes, that's it! >@Override protected void doCommit(DefaultTransactionStatus status) < // . Connection connection = status.getTransaction().getConnectionHolder().getConnection(); try < con.commit(); >catch (SQLException ex) < throw new TransactionSystemException("Could not commit JDBC transaction", ex); >> >

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

Учитывая это, улучшим схему:

  • Если Spring обнаруживает аннотацию @Transactional на бине, он создаёт динамический прокси этого бина.
  • Прокси имеет доступ к менеджеру транзакций и будет просить его открывать и закрывать транзакции/соединения.
  • Сам менеджер транзакций будет просто управлять старым добрым соединением JDBC.

Резюмирую

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

Главным выводом должно быть то, что в итоге не имеет значения, какой фреймворк вы используете, всё дело в основах JDBC.

Транзакции не всегда изолированы друг от друга, поэтому две параллельные транзакции, которые работают с одними и теми же данными, могут оказывать влияние друг на друга. Подробнее об этом читайте в следующей статье ��

Что такое транзакция java

JavaRush — это интерактивный онлайн-курс по изучению Java-программирования c нуля. Он содержит 1200 практических задач с проверкой решения в один клик, необходимый минимум теории по основам Java и мотивирующие фишки, которые помогут пройти курс до конца: игры, опросы, интересные проекты и статьи об эффективном обучении и карьере Java‑девелопера.

Подписывайтесь

Язык интерфейса

«Программистами не рождаются» © 2024 JavaRush

Скачивайте наши приложения

«Программистами не рождаются» © 2024 JavaRush

Этот веб-сайт использует данные cookie, чтобы настроить персонально под вас работу сервиса. Используя веб-сайт, вы даете согласие на применение данных cookie. Больше подробностей — в нашем Пользовательском соглашении.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *