Transactional spring что это
Spring @Transactional — ошибки, которые совершали все
В этой статье я собрал проблемы, с которыми лично сталкивался в проектах. Надеюсь, этот список поможет вам лучше понять транзакции и поспособствует устранению нескольких ваших замечаний.
1. Вызовы в пределах одного класса
@Transactional редко подвергается достаточному количеству тестов, и это приводит к тому, что какие-то проблемы не видны на первый взгляд. В результате вы можете столкнуться со следующим кодом:
Аннотация не работает в методе registerAccount:
В этом случае при вызове registerAccount() сохранение пользователя и создание команды не будут выполняться в одной транзакции. @Transactional работает на основе аспектно-ориентированного программирования. Поэтому обработка происходит, когда один бин вызывается из другого. В приведенном выше примере метод вызывается из того же класса, поэтому прокси не могут быть применены. Так происходит и с другими аннотациями, как, к примеру, @Cacheable.
Проблема может быть решена тремя основными способами:
Самостоятельная инъекция (Self-inject)
Создать еще один уровень абстракции
Первый способ кажется менее очевидным, но таким образом мы избегаем дублирования логики, если @Transactional содержит параметры.
Аннотация работает в методе registerAccount:
2. Обработка не всех исключений
По умолчанию откат происходит только при RuntimeException и Error. В то же время в коде могут встречаться проверяемые исключения, при которых также необходимо откатить транзакцию.
Установите rollbackFor , если вам нужно откатиться назад в случае StripeException:
3. Уровни изоляции транзакций и распространение
Понимание уровней изоляции необходимо для того, чтобы избежать ошибок, которые потом очень трудно отлаживать.
Например, если вы создаете отчеты, то можно выбрать разные данные на уровне изоляции по умолчанию, выполнив один и тот же запрос несколько раз в течение транзакции. Это происходит, когда параллельная транзакция фиксирует что-то в это время. Использование REPEATABLE_READ поможет избежать таких сценариев и сэкономить массу времени на поиск и устранение неисправностей.
4. Транзакции не блокируют данные
Иногда возникает такая конструкция, когда мы выбираем что-то в базе данных, затем обновляем это, и думаем, что поскольку все это делается в транзакции, а транзакции обладают свойством атомарности, то этот код выполняется как один запрос.
Однако проблема в том, что ничто не мешает другому экземпляру приложения вызвать findAllByStatus одновременно с первым. В результате метод вернет одни и те же данные в обоих экземплярах, и их обработка будет произведена 2 раза.
Есть 2 способа избежать этой проблемы.
Выбрать для обновления (пессимистическая блокировка)
Select-for-update в PostgreSQL:
В приведенном выше примере, когда выполняется выбор, строки блокируются до конца обновления. Запрос возвращает все измененные строки.
Версионирование сущностей (оптимистическая блокировка)
5. Два разных источника данных
Например, мы создали новую версию хранилища данных, но все еще должны некоторое время поддерживать старую.
Конечно, в этом случае только один save будет обрабатываться транзакционно, именно в том TransactionalManager, который используется по умолчанию.
Spring предоставляет здесь два варианта.
ChainedTransactionManager
ChainedTransactionManager — это способ объявления нескольких источников данных, в которых, в случае исключения, откат будет происходить в обратном порядке. Таким образом, при наличии трех источников данных, если во время фиксации на втором произошла ошибка, то откат будет произведен только для первых двух. Третий уже зафиксировал изменения.
JtaTransactionManager
Этот менеджер позволяет использовать полностью поддерживаемые распределенные транзакции на основе двухфазной фиксации. Однако он делегирует управление бэкенд-провайдеру JTA. Это могут быть серверы Java EE или отдельные решения (Atomikos, Bitrionix и т.д.).
Заключение
Транзакции — непростая тема, и нередко возникают проблемы с их пониманием. Чаще всего они не полностью покрываются тестами, так что большинство ошибок можно заметить только при ревью кода. А если в продакшне случаются инциденты, найти первопричину всегда непросто.
Всех желающих приглашаем на открытый урок «Репликация». На занятии мы:
— Рассмотрим принцип работы механизмов репликации с точки зрения синхронизации данных;
— Проанализируем проблемы асинхронной репликации и варианты их решения;
— Обсудим предназначение и потенциальные проблемы репликации вида master-master, а также рассмотрим преимущества и недостатки безмастерной репликации.
@Transactional в Spring под капотом
В этой статье рассматривается как работает аннотация @Transactional в Spring под капотом. При этом в основном рассматривается НЕ реактивный стек. Статья рассматривает код Spring 5.3 и Spring Boot 2.4
Оглавление
Где лежит @Transactional и как его добавить в проект?
Для того, чтобы добавить в проект пакет, требуется прописать зависимость:
Кто создает инфраструктуру для обработки @Transactional?
Эта аннотация имеет следующие настройки:
Что же эта аннотация делает? Посмотрим на нее:
С приходом Spring Boot необходимость в аннотации @EnableTransactionManager отпала. Теперь это перешло в ответственность Spring Boot. Как же он это делает?
которая будет загружена при подъеме контекста. Полный текст для удобства приведен ниже.
Как работают авто-конфигурации хорошо объяснено в «SpringBoot-потрошитель», здесь упомяну только наиболее интересные моменты.
Наиболее важной частью является статический класс EnableTransactionManagementConfiguration
Он содержит два подкласса, отличаются они только настройкой
Обратите внимание, что по умолчанию для всех современных spring-boot проектов
Поэтому будет использован механизм CGLIB для создания proxy. (Советую посмотреть интервью, где Борисов рассказывает про это изменение поведения Spring Boot).
Кто обрабатывает @Transactional?
В прошлой части мы закончили на
Посмотрим, что происходит дальше.
Здесь используются следующие классы для конфигураций:
Этот bean применяется для проверки: используется ли где-то @Transactional и для получения метаданных (например, propagation) для аннотированных методов или классов.
Иерархия наследования для класса InfrastructureAdvisorAutoProxyCreator
Обработка @Transactional выполняется по обычным правилам Spring и никакой особой магии здесь нет. Примерная схема работы (взята из официальной документации)
Примерная схема работы @Transactional
Кратко о том как работает proxy или самый популярный вопрос на собеседовании
Во многих фирмах, где проходил собеседование задавали следующий вопрос:
Будет ли при вызове test2 из метода test1 создана новая транзакция?
Самый грамотный и полный ответ, который я встречал, был уже приведен в этой статье на habr. В этой же статье рассмотрим только классический и самый простой случай для лучшего понимания, что происходит дальше.
Под капотом наш bean будет иметь примерно следующий вид
Это прекрасно иллюстрирует этот рисунок
взято с https://www.javainuse.com/spring/spring-boot-aop
Как обрабатываются @Transactional
Прежде чем перейти к рассмотрению порядка обработки, давайте посмотрим, какие настройки предоставляет нам эта аннотация. Здесь рассмотрена только часть из них. Остальные можно посмотреть в этой статье на habr или в документации
Наиболее интересные настройка @Transactional
Выделяется следующие способы:
Правила управления откатом
Порядок работы такой:
выполняем необходимые запросы
Сокращенный код приведен ниже
Если посмотреть внимательно, то этот код повторяет предыдущую логику. Ничего нового:
Рассмотрим внимательно каждую часть.
Управление откатом изменений
Просто выполняется фиксация, если транзакция активна.
Рассмотрим внимательней последний.
Стоит отметить, что в своей работе AbstractPlatformTransactionManager и его подклассы активно используют org. springframework.transaction.support.TransactionSynchronizationManage для синхронизации и хранения метаинформации, включая connection. Хранение информации осуществляется в наборе статических ThreadLocal переменных.
Как выводить логи транзакций?
Также можно узнать, что происходит с транзакциями программно, обращаясь к ThreadLocal переменным, которые мы рассмотрели, например,
Императивная работа с транзакциями (через TransactionTemplate)
который повторяет стандартный механизм rollback/commit:
получаем транзакцию (getTransaction)
выполняем действие (doInTransaction)
если была ошибка, откатываемся (rollbackOnException)
если все хорошо, то фиксируем транзакцию (commit)
Интересна строчка начала транзакции
Обработка ошибок в HibernateTransactionManager
При работе с hibernate, даже если вы поймали ошибку и обработали ее, вы не сможете уже зафиксировать транзакцию, так как она будет помечаться как rollbackOnly (в отличие от работы с JdbcTemplate, например). Как это работает и почему?
Как это работает? Если hibernate ловит ошибку, внутри себя он вызывает
При вызове PlatformTransactionManager.commit получаем статус транзакции, который внутри хранит флаг:
doCommit все равно вызывается и мы могли бы ожидать, что хоть что-то зафиксируется, но hibernate не имеет теперь консистентных данных, поэтому внутри hibernate есть такой код
Эффективное управление транзакциями в Spring
Что ж, конец месяца у нас всегда интенсивные, вот и тут остался всего день до старта второго потока курса «Разработчик на Spring Framework» — замечательного и интересного курса, который ведёт не менее прекрасный и злой Юрий (как его называют некоторые студент за уровень требований в ДЗ), так что давайте рассмотрим ещё один материал, который мы подготовили для вас.
Большую часть времени разработчики не придают значения управлению транзакциями. В результате либо большую часть кода приходится переписывать позже, либо разработчик реализует управление транзакциями без знаний того, как оно на самом деле должно работать или какие аспекты необходимо использовать конкретно в их случае.
Важный аспект в управлении транзакциями — определение правильных границы транзакции, когда транзакция должна начинаться и когда заканчиваться, когда данные должны быть добавлены в БД и когда они должны быть откачены обратно (в случае возникновения исключения).
Самый важный аспект для разработчиков — это понять как реализовать управление транзакциями в приложении наилучшим образом. Поэтому давайте рассмотрим различные варианты.
Способы управления транзакциями
Транзакции могут управляться следующими способами:
1. Программное управление путем написания пользовательского кода
Это старый способ управления транзакциями.
Spring поддерживает два типа управления транзакциями
1. Программное управление транзакциями: Вы должны управлять транзакциями с помощью программирования. Это способ достаточно гибкий, но его сложно поддерживать.
2. Декларативное управление транзакциями: Вы отделяете управление транзакциями от бизнес-логики. Вы используете только аннотации в конфигурации на основе XML для управления транзакциями.
Мы настоятельно рекомендуем использовать декларативные транзакции. Если вы хотите узнать причины, тогда читайте дальше, иначе переходите сразу к разделу Декларативное управление транзакциями, если хотите реализовать этот вариант.
Теперь давайте рассмотрим каждый подход детально.
2.1. Программное управление транзакциями:
Фреймворк Spring предоставляет два средства для программного управления транзакциями.
a. Использование TransactionTemplate (рекомендовано командой Spring):
Давайте рассмотрим как можно реализовать этот тип на примере кода, представленного ниже (взято из документации Spring с некоторыми изменениями)
Обратите внимание, что фрагменты кода взяты из Spring Docs.
Если нет возвращаемого значения, используйте удобный класс TransactionCallbackWithoutResult с анонимным классом, как показано ниже:
Давайте снова посмотрим на эту опцию в коде.
Теперь, перед тем как переходить к следующему способу управления транзакциями, давайте посмотрим как определиться, какой из типов управления транзакциями выбрать.
Выбор между Программным и Декларативным управлением транзакциями:
Шаг 1: Определите менеджер транзакций в контекстном xml файле вашего spring-приложения.
Шаг 2: Включите поддержку аннотаций, добавив запись в контекстном xml файле вашего spring-приложения.
ИЛИ добавьте @EnableTransactionManagement в ваш конфигурационный файл, как показано ниже:
Spring рекомендует аннотировать только конкретные классы (и методы конкретных классов) с аннотацией @Transactional в сравнении с аннотирующими интерфейсами.
Причина этого заключается в том, что вы помещаете аннотацию на уровень интерфейса, и если вы используете прокси-классы ( proxy-target-class = «true» ) или сплетающий аспект ( mode = «aspectj» ), тогда параметры транзакции не распознаются инфраструктурой проксирования и сплетения, например Транзакционное поведение не будет применяться.
Шаг 3: Добавьте аннотацию @Transactional в класс (метод класса) или интерфейс (метод интерфейса).
Конфигурация по умолчанию: proxy-target-class=»false»
READ_COMMITTED (чтение фиксированных данных): Постоянная, указывающая, что “грязное” чтение предотвращено; могут возникать неповторяющееся чтение и фантомное чтение.
READ_UNCOMMITTED (чтение незафиксированных данных): Этот уровень изоляции указывает, что транзакция может считывать данные, которые еще не удалены другими транзакциями.
REPEATABLE_READ (повторяемость чтения): Постоянная, указывающая на то, что “грязное” чтение и неповторяемое чтение предотвращаются; может появляться фантомное чтение.
SERIALIZABLE (упорядочиваемость): Постоянная, указывающая, что “грязное” чтение, неповторяемое чтение и фантомное чтение предотвращены.
Что означают эти жаргонизмы: “грязное” чтение, фантомное чтение или повторяемое чтение?
По умолчанию используется таймаут, установленный по умолчанию для базовой транзакционной системы.
Сообщает менеджеру tx о продолжительности времени, чтобы дождаться простоя tx, прежде чем принять решение об откате не отвечающих транзакций.
Указывает, что целевой метод не может работать без активного tx. Если tx уже запущен до вызова этого метода, то он будет продолжаться в том же tx, или новый tx начнется вскоре после вызова этого метода.
Значение по умолчанию: rollbackFor=RunTimeException.class
В Spring все классы API бросают RuntimeException, это означает, что если какой-либо метод не выполняется, контейнер всегда откатывает текущую транзакцию.
Указывает, что откат не должен происходить, если целевой метод вызывает это исключение.
Теперь последним, но самым важным шагом в управлении транзакциями является размещение аннотации @Transactiona l. В большинстве случаев возникает путаница, где должна размещаться аннотация: на сервисном уровне или на уровне DAO?
@Transactional : Сервисный или DAO уровень?
Существует много CRUD-приложений, у которых нет существенной бизнес-логики, имеющих сервисный уровень, который просто передает данные между контроллерами и объектами доступа к данным, что не является полезным. В этих случаях мы можем поместить аннотацию транзакции на уровень DAO.
Поэтому на практике вы можете поместить их в любом месте, это зависит от вас.
Кроме того, если вы поместите @Transactional в уровень DAO и если ваш уровень DAO будет повторно использоваться разными службами, тогда будет сложно разместить его на уровне DAO, так как разные службы могут иметь разные требования.
Если ваш сервисный уровень извлекает объекты с помощью Hibernate, и, допустим, у вас есть ленивые инициализации в определении объекта домена, тогда вам нужно открыть транзакцию на сервисном уровне, иначе вам придется столкнуться с LazyInitializationException, брошенным ORM.
Рассмотрим другой пример, когда ваш уровень обслуживания может вызывать два разных метода DAO для выполнения операций БД. Если ваша первая операция DAO завершилась неудачей, остальные две могут быть переданы, и вы закончите несогласованное состояние БД. Аннотирование на сервисном уровне может спасти вас от таких ситуаций.
Надеюсь, эта статья вам помогла.
Всегда интересно увидеть ваш комментарии или вопросы.
Транзакции в Spring
Статья описывает пример по работе с транзакциями в популярном framework Spring. Статья предполагает, что вы знакомы с java и spring. Рабочий пример кода можно скачать с sf
Есть две стратегии управления: программное управление (пишем код вручную) и декларативное (настраиваем границы транзакций в XML конфигурациях). В дополнение мы можем работать с разными типами транзакций: локальными и распределенными.
Общая информация по использованию транзакций доступна в документации Spring, но многие сталкиваются с проблемами при попытке применить информацию на практике. Статья содержит рабочий пример для локальных и распределенных транзакция, с использованием обеих стратегий.
Коротко о транзакциях
Под транзакцией мы понимаем ряд действий (не обязательно в БД), которые воспринимаются системой, как единый пакет, т.е. или все действия проходят успешно, или все откатываются на исходные позиции.
Рассмотрим классический пример с переводом денег с одного счета на другой. Перевод денег это наш «пакет», который содержит два действия: уменьшает баланс счета А и увеличивает баланс счета Б. Очевидно, что мы не можем уменьшить первый счет, и провалиться в увеличении второго — в случае ошибки мы должны откатить обе операции.
Как мы говорили ранее есть два типа транзакций — локальные и распределенные. Локальная транзакция работает с одним источником, например одна БД; распределенная использует несколько — например jms source и БД.
Если механизм локальных транзакций прост, то для распределенных требуется определенная инфраструктура. Распределенный процесс требует некого элемента, который будет синхронизировать работу всех источников. Этот элемент — менеджер распределенных транзакций. Подобные менеджеры выпускаются большим количеством компаний, и все они требую определенного изучения документации.
Но есть путь значительно более простой — использовать любой jEE сервер, т.к. они содержат уже настроенные компоненты. В нашем примере мы попробуем использовать JBoss 5.
Для более подробной информации о механизмах транзакций рекомендую посмотреть wikipedia.
Окружение
Во первых нам понадобятся источники для данных: создадим две базы в mysql и назовем их Alfa и Beta. В альфе сделаем две таблицы: alfa1 и alfa2; структуры таблиц одинаковы и содержат поля id и name. Дополнительно добавим уникальный индекс на колонку name для alfa1. В базе Beta создадим только одну таблицу с теми же полями и уникальным индексом.
Обязательно надо указать engine для таблиц — InnoDB, иначе транзакции не будут работать.
В архиве с примером есть скрипт для создания всех структур.
Use case
Наш сценарий содержит две операции: каждая вставляет данные в одну таблицу. Так как таблица alfa2 не содержит индекса, то мы можем вставлять любое количество дубликатов. Дубликаты в остальных таблицах не возможны, и, как следствие, мы должны получать ошибку при повторной вставке и откат транзакции.
Каждый пример надо запускать дважды: первый старт завершится нормально и в обоих таблицах будут новые записи; повторный запуск завершится ошибкой.
Пример локальной транзакции
Локальная транзакция работает с одной базой: Alfa. Подключение к базе описывается в стандартном spring контексте, вместе с описаниями data access layer и сервис layer. Там же указываются конфигурации hibernate.
Data access object содержит два метода для вставки данных (alfa1 alfa2).
Сервис layer использует dao для непосредственной работы.
Для начала попробуем программный метод управления.
Создаем контекст (programContext.xml) и объявляем все бины: подключение к базе, конфигурация hibernate, dao и service. Все бины используют dependency injection.
Самый главный бин для нас — transaction manager. Мы объявляем стандартный класс и используем его в нашем сервисе. В Service указан пример использования — это единственный нюанс.
Так же делаем класс для запуска наших структур, который инициализирует контекст и вызывает наш сервис layer. Первый запуск выполняется нормально и мы видим данные в таблицах alfa1 и alfa2. Повторный запуск вызывает ошибку — так как есть уникальный индекс на alfa1, но откат произойдет и в таблице alfa2, что нам и было необходимо.
В этом подходе нам пришлось явно использовать менеджер транзакций, т.е. наш код стал зависим от внешних библиотек. Мы можем избежать этого используя декларативный подход к управлению транзакциями.
Мы создадим новый объект в service layer, в котором не будет никакого кода по управлению транзакциями, и объявим все бины в новом контексте
(declarativeContext.xml). Подключения к базе, настройки, transaction manager — все это аналогично преведущему примеру. Единственная разница в определении Service, теперь он не использует transaction manager.
Но сами транзакции нам нужны по прежнему, и мы должны информировать об этом spring. В общем, нам надо объявить два элемента: какие методы мы хотим поместить в транзакцию, и какой менеджер использовать.
Правило для определения метода: execution(* service.DeclarativeTx.*(..)) — т.е. мы выбираем все методы нашего service. А следующая настройка говорит, что мы будем использовать менеджер по умолчанию.
Сотрите данные из таблиц и запустите пример дважды. Как обычно, первый запуск выполняется нормально, а второй завершается ошибкой, вместе с откатом всей транзакции.
Пример распределенной транзакции
Большинству приложений достаточно локальных транзакций для успешной работы. Но есть ряд приложений, которые вынужденны работать с данными распределенными по многим источникам. В этом случе для управления изменениями используются распределенные транзакции.
В целом все работает аналогично обычным транзакциям, за исключением того, что нам надо синхронизировать работу всех источников. Тут помогает отдельная подсистема: менеджер управления распределенными транзакциями.
Менеджеры выпускаются большик количеством вендоров в качестве отдельных программных продуктов, т.е. мы должны скачать, установить и сконфигурировать систему. Для этого придется прочитать документацию и тп, но мы выберем путь проще: современные jee серверы приложений содержат встроенные, уже сконфигурированные менеджеры.
Мы будем использовать jboss 5, хотя можно использовать любой другой. Благодаря гибкости spring на не надо делать сервер-зависимых изменений.
Наша цель: создать веб приложение с единственным flow. Контроллер должен вызывать сервисы для вставки данных в обе базы данных. Как обычно, первый показ страницы пройдет успешно, а второй вызовет ошибку с откатом транзакции.
Деплой приложения будет проходить в JBoss, который мы должны предварительно настроить. Как минимум нам нужен доступ в обе базы — создадим два файла для конфигурирования datasources и скопируем их в server/default/deploy. Сами файлы доступны в архиве с примером. Возможно вам надо поменять имя пользователя и пароль, и, самое главное, не забыть положить в lib сервера jdbc драйвер для myysql.
Само приложение так же лежит в архиве. Рассмотрим его подробнее, вместо написание аналога с нуля.
Проект содержит jarы: самого спринга, необходимые для поддержки web, hibernate и тп.
Исходный код в целом аналогичен преведущим примерам: в контексте присутствует два datasource, настроенные через jndi. Присутствуют hibernate мэппинг, dao, serice layer. Декларативное определение транзакций также аналогично ранее приведенному примеру.
Обратите внимание на определение менеджера распределенных транзакций. Ранее мы говорили: мы должны использовать некую внешнюю систему для управления транзакциями. Но никаких конфигураций мы не делаем, благодаря spring. При инициализации spring автоматически найдет доступный менеджер через jndi — т.к. он занет стандартные jndi пути для всех популярных серверов, и последовательно их перебирает.
Дополнительно мы создаем web flow, используя mvc. Наш контроллер взывает сервис для работы с базами данных.
Как обычно, успешно отрабатывает первый запуск (открытие страницы), если страницу перезагрузить — получаем exception и откат транзакции.