1. Введение
В этом руководстве мы рассмотрим аннотацию @Transactional
, а также ее параметры изоляции
и распространения
.
2. Что такое @Transactional?
Мы можем использовать @Transactional
для включения метода в транзакцию базы данных.
Это позволяет нам установить условия распространения, изоляции, тайм-аута, только для чтения и отката для нашей транзакции. Мы также можем указать менеджера транзакций.
2.1. Детали реализации @Transactional
Spring создает прокси или манипулирует байт-кодом класса для управления созданием, фиксацией и откатом транзакции. В случае прокси Spring игнорирует @Transactional
во внутренних вызовах методов.
Проще говоря, если у нас есть такой метод, как callMethod
, и мы помечаем его как @Transactional,
Spring обернет некоторый код управления транзакциями вокруг вызываемого метода @Transactional
, который называется:
createTransactionIfNecessary();
try {
callMethod();
commitTransactionAfterReturning();
} catch (exception) {
completeTransactionAfterThrowing();
throw exception;
}
2.2. Как использовать @Transactional
Мы можем поместить аннотацию в определения интерфейсов, классов или непосредственно в методы. Они переопределяют друг друга в соответствии с порядком приоритета; от низшего к высшему у нас есть: интерфейс, суперкласс, класс, метод интерфейса, метод суперкласса и метод класса.
Spring применяет аннотацию уровня класса ко всем общедоступным методам этого класса, которые мы не аннотировали с помощью @Transactional
.
Однако, если мы поместим аннотацию в закрытый или защищенный метод, Spring проигнорирует ее без ошибки.
Начнем с примера интерфейса:
@Transactional
public interface TransferService {
void transfer(String user1, String user2, double val);
}
Обычно не рекомендуется устанавливать @Transactional
на интерфейсе; однако это приемлемо для таких случаев, как @Repository
с данными Spring. Мы можем поместить аннотацию в определение класса, чтобы переопределить настройку транзакции интерфейса/суперкласса:
@Service
@Transactional
public class TransferServiceImpl implements TransferService {
@Override
public void transfer(String user1, String user2, double val) {
// ...
}
}
Теперь давайте переопределим его, установив аннотацию непосредственно в методе:
@Transactional
public void transfer(String user1, String user2, double val) {
// ...
}
3. Распространение транзакций
Распространение определяет границу транзакции нашей бизнес-логики. Spring удается запустить и приостановить транзакцию в соответствии с нашей настройкой распространения .
Spring вызывает TransactionManager::getTransaction
для получения или создания транзакции в соответствии с распространением. Он поддерживает некоторые распространения для всех типов TransactionManager
, но некоторые из них поддерживаются только конкретными реализациями TransactionManager
.
Давайте рассмотрим различные распространения и как они работают. ``
3.1. ТРЕБУЕТСЯ
Распространение
REQUIRED
— распространение по умолчанию. Spring проверяет, есть ли активная транзакция, и если ничего не существует, создает новую. В противном случае бизнес-логика добавляется к текущей активной транзакции:
@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample(String user) {
// ...
}
Кроме того, поскольку REQUIRED
является распространением по умолчанию, мы можем упростить код, отбросив его:
@Transactional
public void requiredExample(String user) {
// ...
}
Давайте посмотрим на псевдокод того, как работает создание транзакции для REQUIRED
распространения:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return createNewTransaction();
3.2. ПОДДЕРЖИВАЕТ
Распространение
Для SUPPORTS
Spring сначала проверяет, существует ли активная транзакция. Если транзакция существует, то будет использована существующая транзакция. Если транзакции нет, она выполняется нетранзакционно:
@Transactional(propagation = Propagation.SUPPORTS)
public void supportsExample(String user) {
// ...
}
Давайте посмотрим псевдокод создания транзакции для SUPPORTS
:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return emptyTransaction;
3.3. ОБЯЗАТЕЛЬНОЕ
Распространение
При распространении MANDATORY
, если есть активная транзакция, она будет использована. Если активной транзакции нет, Spring выдает исключение:
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample(String user) {
// ...
}
Давайте снова посмотрим на псевдокод:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
throw IllegalTransactionStateException;
3.4. НИКОГДА не
распространять
Для транзакционной логики с распространением НИКОГДА
Spring выдает исключение, если есть активная транзакция:
@Transactional(propagation = Propagation.NEVER)
public void neverExample(String user) {
// ...
}
Давайте посмотрим на псевдокод того, как работает создание транзакции для распространения НИКОГДА
:
if (isExistingTransaction()) {
throw IllegalTransactionStateException;
}
return emptyTransaction;
3.5. NOT_SUPPORTED
Распространение
Если текущая транзакция существует, Spring сначала приостанавливает ее, а затем выполняется бизнес-логика без транзакции:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedExample(String user) {
// ...
}
JTATransactionManager поддерживает реальную приостановку транзакций по умолчанию .
Другие имитируют приостановку, сохраняя ссылку на существующую и затем удаляя ее из контекста потока.
3.6. REQUIRES_NEW
Распространение
Когда распространение равно REQUIRES_NEW
, Spring приостанавливает текущую транзакцию, если она существует, а затем создает новую:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewExample(String user) {
// ...
}
Подобно NOT_SUPPORTED
, нам нужен JTATransactionManager
для фактической приостановки транзакции.
Псевдокод выглядит так:
if (isExistingTransaction()) {
suspend(existing);
try {
return createNewTransaction();
} catch (exception) {
resumeAfterBeginException();
throw exception;
}
}
return createNewTransaction();
3.7. ВЛОЖЕННОЕ
Распространение
Для распространения NESTED
Spring проверяет, существует ли транзакция, и если да, то отмечает точку сохранения. Это означает, что если выполнение нашей бизнес-логики выдает исключение, транзакция откатывается к этой точке сохранения. Если активной транзакции нет, она работает как REQUIRED
.
DataSourceTransactionManager
поддерживает это распространение по умолчанию. Некоторые реализации JTATransactionManager
также могут поддерживать это.
**JpaTransactionManager
поддерживает NESTED
только для соединений JDBC. Однако если мы установим для флага nestedTransactionAllowed значение
true
, он также будет работать для кода доступа JDBC в транзакциях JPA, если наш драйвер JDBC поддерживает точки сохранения.
**
Наконец, давайте установим
распространение
NESTED : ``
@Transactional(propagation = Propagation.NESTED)
public void nestedExample(String user) {
// ...
}
4. Изоляция транзакций
Изоляция является одним из общих свойств ACID: атомарность, согласованность, изоляция и долговечность. Изоляция описывает, как изменения, применяемые параллельными транзакциями, видны друг другу.
Каждый уровень изоляции предотвращает нулевые или более побочные эффекты параллелизма в транзакции:
- Грязное чтение: чтение незафиксированного изменения параллельной транзакции.
- Неповторяемое чтение : получить другое значение при повторном чтении строки, если параллельная транзакция обновляет ту же строку и фиксирует
- Фантомное чтение: получить разные строки после повторного выполнения запроса диапазона, если другая транзакция добавляет или удаляет некоторые строки в диапазоне и фиксирует
Мы можем установить уровень изоляции транзакции с помощью @Transactional::isolation.
В Spring есть пять перечислений: DEFAULT
, READ_UNCOMMITTED
, READ_COMMITTED
, REPEATABLE_READ
, SERIALIZABLE.
4.1. Управление изоляцией весной
Уровень изоляции по умолчанию — DEFAULT
. В результате, когда Spring создает новую транзакцию, уровнем изоляции будет уровень изоляции по умолчанию для нашей СУБД. Поэтому мы должны быть осторожны, если мы изменим базу данных.
Также следует рассмотреть случаи, когда мы вызываем цепочку методов с разной изоляцией .
В обычном потоке изоляция применяется только при создании новой транзакции. Таким образом, если по какой-либо причине мы не хотим, чтобы метод выполнялся в другой изоляции, мы должны установить для TransactionManager::setValidateExistingTransaction
значение true.
Тогда псевдокод проверки транзакции будет таким:
if (isolationLevel != ISOLATION_DEFAULT) {
if (currentTransactionIsolationLevel() != isolationLevel) {
throw IllegalTransactionStateException
}
}
Теперь давайте углубимся в различные уровни изоляции и их эффекты.
4.2. READ_UNCOMMITTED
Изоляция
READ_UNCOMMITTED
— это самый низкий уровень изоляции, обеспечивающий наиболее одновременный доступ.
В результате он страдает от всех трех упомянутых побочных эффектов параллелизма. Транзакция с такой изоляцией считывает незафиксированные данные других параллельных транзакций. Кроме того, могут происходить как неповторяющиеся, так и фантомные чтения. Таким образом, мы можем получить другой результат при повторном чтении строки или повторном выполнении запроса диапазона.
Мы можем установить уровень изоляции
для метода или класса:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void log(String message) {
// ...
}
Postgres не поддерживает изоляцию READ_UNCOMMITTED
и вместо этого возвращается к READ_COMMITED
. **Кроме того, Oracle не поддерживает и не разрешает READ_UNCOMMITTED
.
**
4.3. READ_COMMITTED
Изоляция
Второй уровень изоляции, READ_COMMITTED,
предотвращает грязное чтение.
Остальные побочные эффекты параллелизма все еще могут произойти. Таким образом, незафиксированные изменения в параллельных транзакциях не влияют на нас, но если транзакция фиксирует свои изменения, наш результат может измениться при повторном запросе.
Здесь мы устанавливаем уровень изоляции
:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void log(String message){
// ...
}
READ_COMMITTED
— это уровень по умолчанию для Postgres, SQL Server и Oracle.
4.4. REPEATABLE_READ
Изоляция
Третий уровень изоляции, REPEATABLE_READ,
предотвращает грязное и неповторяющееся чтение. Таким образом, на нас не влияют незафиксированные изменения в параллельных транзакциях.
Кроме того, когда мы повторно запрашиваем строку, мы не получаем другого результата. Однако при повторном выполнении range-запросов мы можем получить вновь добавленные или удаленные строки.
Более того, это самый низкий уровень, необходимый для предотвращения потери обновления. Потерянное обновление происходит, когда две или более параллельных транзакций читают и обновляют одну и ту же строку. REPEATABLE_READ
вообще не разрешает одновременный доступ к строке. Следовательно, потерянное обновление не может произойти.
Вот как установить уровень изоляции
для метода:
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void log(String message){
// ...
}
REPEATABLE_READ
— это уровень по умолчанию в Mysql. Oracle не поддерживает REPEATABLE_READ
.
4.5. СЕРИАЛИЗУЕМАЯ
Изоляция
SERIALIZABLE
— это самый высокий уровень изоляции. Это предотвращает все упомянутые побочные эффекты параллелизма, но может привести к самой низкой скорости параллельного доступа, поскольку он выполняет параллельные вызовы последовательно.
Другими словами, одновременное выполнение группы сериализуемых транзакций имеет тот же результат, что и их последовательное выполнение.
Теперь давайте посмотрим, как установить SERIALIZABLE
в качестве уровня изоляции
:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void log(String message){
// ...
}
5. Вывод
В этой статье мы подробно рассмотрели свойство распространения @Transaction
. Затем мы узнали о побочных эффектах параллелизма и уровнях изоляции.
Как всегда, полный код для этой статьи доступен на GitHub .