Spring @Transactional - Изоляция и Пропагация
Заголовок: Объясните параметры "isolation" и "propagation" в аннотации @Transactional с реальным примером
Описание проблемы:
Привет всем!
Я пытаюсь понять, как работают параметры isolation и propagation в аннотации @Transactional
в контексте управления транзакциями в Spring. Можете ли вы привести реальный пример, чтобы проиллюстрировать, как и когда следует изменять их значения по умолчанию?
В частности, мне интересно, когда и почему я должен изменять эти параметры? Какие сценарии использования требуют изменения значений изначально заданных по умолчанию?
Буду признателен за любые советы и пояснения!
5 ответ(ов)
На русском языке и в стиле ответа на StackOverflow, информация будет выглядеть следующим образом:
PROPAGATION_REQUIRED = 0: Если для метода M1 уже запущен объект транзакции DataSourceTransactionObject T1, то для другого метода M2, требующего объект транзакции, не создается новый объект. В этом случае используется тот же самый объект T1 для метода M2.
PROPAGATION_MANDATORY = 2: Метод должен выполняться в контексте транзакции. Если в данный момент нет активной транзакции, будет выброшено исключение.
PROPAGATION_REQUIRES_NEW = 3: Если объект транзакции DataSourceTransactionObject T1 уже запущен для метода M1 и в процессе его выполнения, а затем начинается выполнение метода M2, то T1 приостанавливается на время выполнения метода M2. Для метода M2 создается новый объект DataSourceTransactionObject T2, который будет работать в своем собственном контексте транзакции.
PROPAGATION_NOT_SUPPORTED = 4: Если для метода M1 уже запущен объект транзакции DataSourceTransactionObject T1 и одновременно выполняется метод M2, то метод M2 не должен выполняться в контексте транзакции, и T1 приостанавливается до завершения метода M2.
PROPAGATION_NEVER = 5: Никакие методы не выполняются в контексте транзакции.
Уровень изоляции: Этот параметр определяет, насколько транзакция может быть затронута действиями других параллельных транзакций. Он обеспечивает согласованность данных, оставляя данные в нескольких таблицах в согласованном состоянии. Включает блокировку строк и/или таблиц в базе данных.
Проблемы с многими транзакциями:
Сценарий 1: Если транзакция T1 считывает данные из таблицы A1, которые были записаны другой параллельной транзакцией T2, и в процессе T2 откатывается, то данные, полученные T1, будут недействительными. Например, если a=2 — это исходные данные, а T1 считал a=1, записанное T2, то после отката T2 значение a=1 будет возвращено к a=2 в БД, однако T1 будет продолжать иметь a=1.
Сценарий 2: Если транзакция T1 считывает данные из таблицы A1, а другая параллельная транзакция (T2) обновляет данные в той же таблице, то данные, прочитанные T1, будут отличаться от данных в A1. Например, если T1 считал a=1, а T2 обновил a=2, то данное значение будет неконсистентным.
Сценарий 3: Если T1 считывает данные из таблицы A1 с определенным количеством строк, а другая параллельная транзакция T2 вставляет новые строки в таблицу A1, то количество строк, считанных T1, будет отличаться от фактического количества строк в таблице A1.
Сценарий 1 называется Грязные чтения (Dirty reads).
Сценарий 2 называется Неповторяемые чтения (Non-repeatable reads).
Сценарий 3 называется Призрачные чтения (Phantom reads).
Уровень изоляции определяет, насколько возможно предотвратить Сценарий 1, Сценарий 2 и Сценарий 3. Полная изоляция достигается путем реализации блокировок, чтобы предотвратить одновременные чтения и записи на одни и те же данные, но это влияет на производительность. Уровень изоляции зависит от конкретного приложения и необходимой степени изоляции.
ISOLATION_READ_UNCOMMITTED: Позволяет читать изменения, которые еще не были зафиксированы. Он подвержен всем сценариям.
ISOLATION_READ_COMMITTED: Позволяет чтения только из успешно завершенных параллельных транзакций. Он может быть подвержен Сценарию 2 и Сценарию 3, так как другие транзакции могут обновлять данные.
ISOLATION_REPEATABLE_READ: Множественные чтения одного и того же поля будут возвращать одинаковые результаты, пока это поле не изменится. Он может быть подвержен Сценарию 3 из-за возможного вставления данных другими транзакциями.
ISOLATION_SERIALIZABLE: Сценарий 1, Сценарий 2 и Сценарий 3 никогда не произойдут. Это полная изоляция, включающая полную блокировку, что, в свою очередь, влияет на производительность из-за блокирования.
Вы можете протестировать поведение транзакций с помощью следующего кода:
public class TransactionBehaviour {
// Настройка выполняется либо через XML, либо через аннотации
DataSourceTransactionManager manager = new DataSourceTransactionManager();
SimpleTransactionStatus status = new SimpleTransactionStatus();
public void beginTransaction() {
DefaultTransactionDefinition Def = new DefaultTransactionDefinition();
// Переопределите уровень PROPAGATION_REQUIRED и ISOLATION_DEFAULT
// Настройка выполняется либо через XML, либо через аннотации
manager.setPropagationBehavior(XX);
manager.setIsolationLevelName(XX);
status = manager.getTransaction(Def);
}
public void commitTransaction() {
if (status.isCompleted()) {
manager.commit(status);
}
}
public void rollbackTransaction() {
if (!status.isCompleted()) {
manager.rollback(status);
}
}
public static void main(String[] args) {
beginTransaction();
M1();
if (error()) {
rollbackTransaction();
}
commitTransaction();
}
}
Вы можете отладить и увидеть результаты с разными значениями для изоляции и распространения транзакций.
Достаточно объяснений о каждом параметре дано в других ответах. Однако вы попросили реальный пример, и вот он, который проясняет назначение различных параметров пропагации:
Предположим, вы отвечаете за реализацию сервиса регистрации, в котором пользователю отправляется подтверждающее электронное письмо. Вы создаете два сервисных объекта: один для регистрации пользователя и другой для отправки электронных писем; последний вызывается внутри первого. Например, это может выглядеть так:
/* Сервис регистрации */
@Service
@Transactional(Propagation=REQUIRED)
class SignUpService {
...
void signUp(User user) {
...
emailService.sendMail(user);
}
}
/* Сервис электронной почты */
@Service
@Transactional(Propagation=REQUIRES_NEW)
class EmailService {
...
void sendMail(User user) {
try {
... // Попытка отправить электронное письмо
} catch (Exception e) {
...
}
}
}
Вы могли заметить, что второй сервис имеет тип пропагации REQUIRES_NEW, и, кроме того, есть вероятность, что он выбросит исключение (например, если SMTP сервер не работает, невалидный адрес электронной почты или по другим причинам). Вы, вероятно, не хотите, чтобы весь процесс был отменен, например, удаление информации о пользователе из базы данных или другие действия; поэтому вы вызываете второй сервис в отдельной транзакции.
Возвращаясь к нашему примеру, на этот раз вы заботитесь о безопасности базы данных, поэтому определяете ваши DAO классы следующим образом:
/* DAO пользователя */
@Transactional(Propagation=MANDATORY)
class UserDAO {
// некоторые CRUD методы
}
Это означает, что всякий раз, когда создается объект DAO, а следовательно, потенциальный доступ к базе данных, вам нужно убедиться, что вызов был выполнен изнутри одного из ваших сервисов, что подразумевает наличие активной транзакции; в противном случае возникает исключение. Поэтому тип пропагации установлен как MANDATORY.
Вы почти никогда не хотите использовать Read Uncommitted
, так как это не соответствует требованиям ACID
. Read Committed
— хорошая отправная точка по умолчанию. Repeatable Read
вероятно, нужен лишь в сценариях отчетности, агрегации или сводки данных. Обратите внимание, что многие базы данных, включая Postgres, на самом деле не поддерживают Repeatable Read
; вам нужно использовать Serializable
вместо него. Serializable
полезен для операций, которые должны выполняться полностью независимо от других, подумайте об этом как о synchronized
в Java. Serializable
идет рука об руку с пропагандой REQUIRES_NEW
.
Я использую REQUIRES
для всех функций, которые выполняют запросы UPDATE или DELETE, а также для "сервисных" функций. Для DAO-функций, выполняющих только SELECT-запросы, я использую SUPPORTS
, который будет участвовать в транзакции, если она уже начата (т.е. если вызывается из сервисной функции).
Перевод вашего вопроса на русский в стиле ответа на StackOverflow:
Я запустил методы outerMethod
, method_1
и method_2
с различными режимами распространения транзакций. Вот результаты выполнения для разных режимов.
Внешний метод (outerMethod
)
@Transactional
@Override
public void outerMethod() {
customerProfileDAO.method_1();
iWorkflowDetailDao.method_2();
}
Метод 1 (method_1
)
@Transactional(propagation=Propagation.MANDATORY)
public void method_1() {
Session session = null;
try {
session = getSession();
Temp entity = new Temp(0l, "XXX");
session.save(entity);
System.out.println("Method - 1 Id " + entity.getId());
} finally {
if (session != null && session.isOpen()) {
// Закрытие сессии
}
}
}
Метод 2 (method_2
)
@Transactional()
@Override
public void method_2() {
Session session = null;
try {
session = getSession();
Temp entity = new Temp(0l, "CCC");
session.save(entity);
int i = 1 / 0; // Искусственная ошибка
System.out.println("Method - 2 Id " + entity.getId());
} finally {
if (session != null && session.isOpen()) {
// Закрытие сессии
}
}
}
Результаты выполнения:
- Внешний метод - без транзакции
method_1
-Propagation.MANDATORY
method_2
- только аннотация@Transactional
- Вывод:
method_1
выбросит исключение, так как не существует активной транзакции.
- Внешний метод - без транзакции
method_1
- только аннотация@Transactional
method_2
-Propagation.MANDATORY
- Вывод:
method_2
выбросит исключение, так как не существует активной транзакции.
method_1
успешно выполнит сохранение записи в базе данных.
- Внешний метод - с транзакцией
method_1
- только аннотация@Transactional
method_2
-Propagation.MANDATORY
- Вывод: Обе записи будут сохранены в базе данных, так как используется существующая транзакция для
method_1
иmethod_2
.
- Внешний метод - с транзакцией
method_1
-Propagation.MANDATORY
method_2
- только аннотация@Transactional
и выброс исключения- Вывод: Записи не сохраняются в базе данных, значит произошел откат.
- Внешний метод - с транзакцией
method_1
-Propagation.REQUIRES_NEW
method_2
-Propagation.REQUIRES_NEW
и выброс исключения (1/0)- Вывод:
method_2
выбросит исключение, поэтому запись изmethod_2
не будет сохранена.
method_1
успешно сохранит запись в базу данных. Откат дляmethod_1
не произойдет.
Если вам нужно больше пояснений по каждому из случаев, пожалуйста, дайте знать!
Вы можете добавить аннотацию @Transactional
к вашему классу Banking_CustomerService
, чтобы управлять транзакциями и их свойствами, как в приведенном ниже примере:
@Transactional(readOnly = true)
public class Banking_CustomerService implements CustomerService {
public Customer getDetail(String customername) {
// здесь выполняем какие-то действия
}
// для этого метода настройки транзакции имеют более высокий приоритет
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateCustomer(Customer customer) {
// здесь выполняем какие-то действия
}
}
В этом примере класс Banking_CustomerService
помечен аннотацией @Transactional(readOnly = true)
, что означает, что все методы этого класса будут работать в режиме "только для чтения" по умолчанию. Однако для метода updateCustomer
мы переопределяем это поведение, добавляя аннотацию @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
. Это указывает, что метод может производить изменения и запускать новую транзакцию.
Таким образом, при вызове метода getDetail
транзакция не будет изменять состояние базы данных, тогда как метод updateCustomer
будет обновлять данные, начиная новую транзакцию.
Где находится аннотация @Transactional?
Создание репозитория Spring без сущности
Почему нет ConcurrentHashSet, если есть ConcurrentHashMap?
Как объявить массив в одну строку?
Загрузка JDK Java на Linux через wget приводит к отображению страницы лицензии вместо установки