Java Transaction API¶
Что происходит, когда ты вешаешь @Transactional на сервисный метод
- Клиент вызывает метод сервиса (на самом деле его прокси).
- Контейнер (через AOP/интерцептор) видит аннотацию @Transactional.
- Смотрит атрибут (по умолчанию REQUIRED):
- если нет активной транзакции:
- создаёт новую транзакцию в TxManager;
- если есть открытая транзакция (например, этот сервис вызван из другого @Transactional):
- входит в уже существующую (не создаёт новую).
- Внутри метода:
- когда код впервые трогает EntityManager/DataSource,
- контейнер берёт подключение из пула и ассоциирует его с текущей транзакцией.
- Весь последующий код, который работает с:
- тем же EntityManager,
- другими DAO в рамках того же потока, видит одну и ту же транзакцию.
- Когда метод успешно завершается без исключений:
- интерцептор говорит TxManager’у: «Ок, коммитим»:
- если это корневая транзакция (мы её открывали) → делает commit,
- если мы внутри уже существующей (REQUIRED) → просто говорит «я ок, но коммит после выхода из внешнего уровня».
- Если из метода вылетает неконтролируемое (runtime) исключение:
- транзакция помечается как rollback-only,
- при выходе интерцептор делает rollback.
Поэтому: да — все вызовы DAO/EntityManager внутри этого метода выполняются в рамках одной большой транзакции (и вложенных сервисов тоже, если они не ломают семантику).
@Transactional (атрибуты) - поведение транзакции
REQUIRED(по умолчанию):- если транзакции нет → создать новую,
- если есть → использовать её.
REQUIRES_NEW:- всегда создать новую,
- если была — приостановить старую на время выполнения метода.
MANDATORY:- метод должен вызываться только внутри уже существующей транзакции,
- иначе бросается ошибка.
SUPPORTS:- если транзакция есть → использовать,
- если нет → работать без транзакции.
NOT_SUPPORTED:- если есть транзакция → приостановить её на время метода,
- внутри метода к БД — без транзакции.
NEVER:- метод не должен выполняться в транзакции,
- если есть активная — ошибка.
Управление параллелизмом и блокировками стратегии управления конкурентным доступом к данным при параллельных транзакциях Если транзакции работают параллельно и обращаются к одному ресурсу (строка в бд): база данных применяет блокировки
Pessimistic Locking - гарантирует, что другие транзакции не могут получить доступ к данным, пока текущая транзакция не завершится. Предполагает, что с высокой вероятностью данные будут изменяться параллельно, поэтому мы сразу блокируем доступ к данным, чтобы другие транзакции не могли их изменять.
Optimistic Locking - при её использовании транзакции не блокируют ресурсы, но перед завершением проверяют, что данные не были изменены другой транзакцией. В JPA это делается через версионность сущностей. При изменении сущности Spring будет проверять версию, чтобы убедиться, что никто не изменил её в другом процессе. Предполагает, что конфликтов не будет, и мы позволяем транзакциям работать параллельно. Однако, перед сохранением данных мы проверяем, не был ли конфликт — например, что кто-то другой не изменил данные, с которыми работала текущая транзакция.
Обработка ошибок и откаты транзакций:
Когда транзакция не удаётся (например, из-за параллельной блокировки или ошибки в коде), она должна быть откатана. В
Spring ты можешь настроить откат для транзакции при возникновении исключений:
@Transactional(rollbackFor = Exception.class)
Pessimistic Locking¶
Идея: Мы «пессимистично» предполагаем, что два пользователя могут одновременно пытаться изменить одни и те же данные. Поэтому мы блокируем эти данные на время выполнения транзакции, чтобы другие транзакции не могли их изменить, пока первая не завершится.
Как это работает:
- Когда транзакция запрашивает данные для изменения, она накладывает на них блокировку.
- Пока блокировка не будет снята (после коммита или отката транзакции), другие транзакции не смогут получить доступ к этим данным.
-
Это подходит для случаев, когда нужно гарантировать, что данные не будут изменены параллельно, например, для работы с банковскими счетами, чтобы избежать ошибок, связанных с потерей обновлений.
-
Блокировка накладывается при запросе данных для изменения. Когда ты пытаешься получить сущность для редактирования с использованием пессимистичной блокировки, например, с LockModeType.PESSIMISTIC_WRITE, база данных не только загружает данные, но и блокирует эти строки или записи.
- Например, когда ты выполняешь запрос с PESSIMISTIC_WRITE, база данных блокирует строки так, чтобы никакие другие транзакции не могли изменять их до завершения текущей транзакции.
Что происходит, если другая транзакция пытается получить доступ к тем же данным?
- В отличие от Optimistic Locking, где транзакция просто проверяет, не было ли изменений, Pessimistic Locking не позволяет другой транзакции получить доступ к данным, пока текущая транзакция не завершится.
- Вторая транзакция будет ждать, пока первая не завершится (commit или rollback). Это поведение можно считать блокировкой на уровне базы данных.
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
// Получаем счета с пессимистичной блокировкой для записи
Account accountFrom = entityManager.find(Account.class, from.getId(), LockModeType.PESSIMISTIC_WRITE);
Account accountTo = entityManager.find(Account.class, to.getId(), LockModeType.PESSIMISTIC_WRITE);
// Проводим операции
if (accountFrom.getBalance().compareTo(amount) >= 0) {
accountFrom.setBalance(accountFrom.getBalance().subtract(amount));
accountTo.setBalance(accountTo.getBalance().add(amount));
} else {
throw new InsufficientFundsException("Недостаточно средств.");
}
}
- LockModeType.PESSIMISTIC_WRITE: Говорит JPA, что нужно наложить блокировку на строки этих сущностей, чтобы другие транзакции не могли их изменять, пока текущая транзакция не завершится.
- В данном примере, если кто-то ещё захочет заблокировать или изменить эти счета, он будет вынужден ждать, пока текущая транзакция не завершится.
Здесь первая транзакция блокирует записи с помощью PESSIMISTIC_WRITE, и если вторая транзакция попытается обратиться к этим же данным, она будет ждать, пока первая транзакция не завершится. Транзакции будут выполняться по очереди.
Важно:
Пессимистичная блокировка гарантирует, что параллельно транзакции не смогут изменять одну и ту же запись. Если одна транзакция работает с записью, другая транзакция будет вынуждена ждать, пока первая не завершит свою работу.
Преимущества и недостатки Pessimistic Locking:
Преимущества:
- Гарантирует, что данные не будут изменены параллельно.
- Очень полезно, когда есть высокая вероятность конфликтов (например, с балансовыми счетами).
Недостатки:
- Может приводить к deadlock (взаимная блокировка), если два процесса блокируют друг друга.
- Проблемы с производительностью: В системе с высокой конкурентностью, где транзакции часто обращаются к одной и той же записи, это может сильно замедлить работу.
Optimistic Locking¶
Идея: Мы «оптимистично» предполагаем, что параллельных изменений данных будет мало, и даём транзакциям работать параллельно. Однако перед сохранением данных мы проверяем, не был ли изменён объект другими транзакциями.
Как это работает:
- Когда транзакция загружает данные, она не блокирует их.
- Когда она пытается сохранить изменения, система проверяет, были ли данные изменены с момента их загрузки. Для этого обычно используется версия или временная метка.
- Если данные были изменены другими транзакциями, текущая транзакция не может их сохранить, и происходит ошибка конфликтов.
В Optimistic Locking нет явной блокировки. Каждая транзакция параллельно изменяет данные, но перед тем, как закоммитить изменения, она проверяет, не была ли запись изменена другим пользователем (или другой транзакцией) после того, как она её загрузила.
Что происходит, если происходит конфликт?
- В случае конфликта версий (если запись была изменена в другой транзакции после того, как первая транзакция её загрузила), вторая транзакция не сможет выполнить commit, потому что версия записи не совпадет.
- Это приводит к ошибке (обычно OptimisticLockException в JPA) и транзакция не может быть выполнена.
Для того чтобы избежать этой ошибки, обычно нужно повторить транзакцию (retry).
@Entity
public class Account {
@Id
private Long id;
@Version
private Long version; // Столбец для отслеживания версии
private BigDecimal balance;
// геттеры и сеттеры
}
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
Account accountFrom = entityManager.find(Account.class, from.getId());
Account accountTo = entityManager.find(Account.class, to.getId());
if (accountFrom.getBalance().compareTo(amount) >= 0) {
accountFrom.setBalance(accountFrom.getBalance().subtract(amount));
accountTo.setBalance(accountTo.getBalance().add(amount));
// Обновляем запись. Оптимистичная блокировка проверит версию
} else {
throw new InsufficientFundsException("Недостаточно средств.");
}
}
- @Version: Этот столбец будет использоваться для отслеживания изменений. Каждый раз, когда сущность обновляется, значение версии увеличивается.
- Если при попытке сохранить сущность версия на сервере не совпадает с версией в базе данных, это означает, что данные были изменены другой транзакцией.
Преимущества и недостатки Optimistic Locking:
Преимущества:
- Лучшая производительность при низкой вероятности конфликтов, так как нет блокировок на данные.
- Используется в системах с высокой конкурентностью, где редко происходят параллельные изменения.
Недостатки:
- Если конфликт происходит, транзакция должна быть повторена.
- Не подходит для всех типов данных (например, баланс счёта, где всегда важен порядок операций).
ЖЦ¶
Сценарий 1: Pessimistic Locking
- Транзакция 1: Пользователь A пытается перевести деньги с одного счёта на другое.
- Загружает оба счёта с пессимистичной блокировкой: LockModeType.PESSIMISTIC_WRITE.
- Считает баланс и выполняет операцию.
- Транзакция блокирует эти записи до конца операции.
- Транзакция 2: Пользователь B пытается сделать то же самое (или на одном из тех же счетов).
- Когда он пытается изменить те же счёта, транзакция ждёт, пока не будет освобождена блокировка первой транзакцией.
- Результат: После того, как первая транзакция завершена (commit), вторая транзакция продолжает свою работу.
Сценарий 2: Optimistic Locking
- Транзакция 1: Пользователь A загружает данные счёта и пытается провести операцию, изменив баланс.
- В это время версия данных счёта фиксируется и хранится.
- Транзакция выполняет изменения и пытается сохранить данные.
- Транзакция 2: Пользователь B делает то же самое и меняет баланс того же счёта.
- Его версия счёта также будет зафиксирована.
- Когда пользователь A пытается сохранить изменения, проверка версии покажет, что данные были изменены пользователем B.
- Транзакция A не будет успешно сохранена, и произойдёт ошибка версии.
- Результат: Транзакция A должна быть повторена, так как данные изменились в другой транзакции.