1. Какую проблему решить
Начнем с транзакций: «Что такое транзакция? Зачем нам транзакция?». Транзакция — это набор операций, которые нельзя разделить, либо все они завершатся успешно, либо все они не пройдут. В разработке нам необходимо объединить некоторые операции в блок посредством транзакций, чтобы обеспечить корректность логики программы, например успешную вставку всех, или откат, и ни одна из них не вставляется. Как программисты, для управления транзакциями все, что нам нужно сделать, этоопределение бизнеса, то есть чем-то вродеbegin transaction
а такжеend transaction
операции для разграничения начала и конца транзакции.
Ниже приведен базовый код управления транзакциями JDBC:
// 开启数据库连接
Connection con = openConnection();
try {
// 关闭自动提交
con.setAutoCommit(false);
// 业务处理
// ...
// 提交事务
con.commit();
} catch (SQLException | MyException e) {
// 捕获异常,回滚事务
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
// 关闭连接
try {
con.setAutoCommit(true);
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
Непосредственное использование JDBC для кода управления транзакциями. Интуитивно возникают две проблемы:
- Код бизнес-процессинга смешивается с кодом управления транзакциями;
- Много кода обработки исключений (и try-catch в catch).
Если нам нужно заменить другие технологии доступа к данным, такие как Hibernate, MyBatis, JPA и т. д., хотя операции управления транзакциями аналогичны, но API отличается, нам нужно использовать соответствующий API для перезаписи. Это также приводит к третьему вопросу:
- Сложный API управления транзакциями.
Три проблемы, которые необходимо решить, перечислены выше, давайте посмотрим, как решаются транзакции Spring.
2. Как решить
2.1 API управления сложными транзакциями
В ответ на эту проблему мы можем легко подумать об абстрагировании уровня API для управления многими транзакциями. пройти черезопределить интерфейсЗащитите конкретную реализацию, а затем используйтережим стратегиидля определения конкретного API. Давайте взглянем на абстрактные интерфейсы, определенные в транзакциях Spring.
В транзакциях Spring основным интерфейсом являетсяPlatformTransactionManager
, также называемый диспетчером транзакций, который определяется следующим образом:
public interface PlatformTransactionManager extends TransactionManager {
// 获取事务(新的事务或者已经存在的事务)
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
getTransaction
введяTransactionDefinition
получитьTransactionStatus
, то есть соответствующий объект транзакции создается посредством определенной метаинформации транзакции. существуетTransactionDefinition
будет содержатьМетаинформация для транзакций:
- PropagationBehavior: поведение распространения;
- IsolationLevel: уровень изоляции;
- Тайм-аут: время тайм-аута;
- ReadOnly: Доступен ли он только для чтения.
согласно сTransactionDefinition
приобретенныйTransactionStatus
ЦКинкапсулировать объект транзакции, и обеспечиваетоперационная сделкаа такжеПосмотреть статус транзакцииметод, например:
-
setRollbackOnly
: пометить транзакцию как «Только для отката», чтобы ее можно было откатить; -
isRollbackOnly
: Проверьте, помечен ли он как «Только для отката»; -
isCompleted
: чтобы увидеть, завершена ли транзакция (выполнена фиксация или откат).
Также поддерживаются родственные методы для вложенных транзакций:
-
createSavepoint
: создать точку сохранения; -
rollbackToSavepoint
: Откат к указанной точке сохранения; -
releaseSavePoint
: освободить точку сохранения.
TransactionStatus
Объекты транзакции могут быть переданы вcommit
метод илиrollback
метод, завершите фиксацию или откат транзакции.
Ниже мы разбираемся через конкретную реализациюTransactionStatus
эффект. кcommit
метод в качестве примера, как пройтиTransactionStatus
Завершите фиксацию транзакции.AbstractPlatformTransactionManager
даPlatformTransactionManager
Реализация интерфейса в виде шаблонного класса, которыйcommit
Реализация выглядит следующим образом:
public final void commit(TransactionStatus status) throws TransactionException {
// 1.检查事务是否已完成
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
// 2.检查事务是否需要回滚(局部事务回滚)
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}
// 3.检查事务是否需要回滚(全局事务回滚)
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}
// 4.提交事务
processCommit(defStatus);
}
существуетcommit
шаблонный методОсновная логика фиксации транзакции определена вstatus
Состояние транзакции для принятия решения о создании исключения, откате или фиксации. один из нихprocessRollback
а такжеprocessCommit
Этот метод также является шаблонным методом, который дополнительно определяет логику отката и фиксации. кprocessCommit
метод в качестве примера, конкретная операция отправки будет обрабатываться абстрактным методомdoCommit
Заканчивать.
protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;
doCommit
Реализация зависит от конкретной технологии доступа к данным. Давайте взглянем на соответствующие конкретные классы реализации JDBC.DataSourceTransactionManager
серединаdoCommit
выполнить.
protected void doCommit(DefaultTransactionStatus status) {
// 获取status中的事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
// 通过事务对象获得数据库连接对象
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
// 执行commit
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
существуетcommit
а такжеprocessCommit
В методе мы опираемся на входные параметрыTransactionStatus
который предоставилстатус транзакциидля определения транзакционного поведения, в то время как вdoCommit
Когда фиксация транзакции должна быть выполнена вTransactionStatus
серединаобъект транзакциичтобы получить объект подключения к базе данных, а затем выполнить окончательныйcommit
работать. На этом примере мы можем понятьTransactionStatus
Предоставленный статус транзакции и роль объекта транзакции.
Ниже приведен код управления транзакциями, переписанный с использованием Spring API транзакций:
// 获得事务管理器
PlatformTransactionManager txManager = getPlatformTransactionManager();
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 指定事务元信息
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 获得事务
TransactionStatus status = txManager.getTransaction(def);
try {
// 业务处理
}
catch (MyException ex) {
// 捕获异常,回滚事务
txManager.rollback(status);
throw ex;
}
// 提交事务
txManager.commit(status);
Независимо от того, используете ли вы JDBC, Hibernate или MyBatis, нам просто нужно передатьtxManager
Соответствующая конкретная реализация может переключаться между различными технологиями доступа к данным.
Резюме: транзакции Spring проходят
PlatformTransactionManager
,TransactionDefinition
а такжеTransactionStatus
Интерфейс объединяет API управления транзакциями и объединяет шаблон стратегии и метод шаблона для определения конкретной реализации.
Вы нашли еще одну особенность кода API транзакций Spring?SQLException
ушел. Давайте посмотрим, как транзакции Spring решают много кода обработки исключений.
2.2 Много кода обработки исключений
Зачем нужно писать так много кода обработки исключений в коде, использующем JDBC? Это потому чтоConnection
Каждый метод будет бросатьSQLException
,а такжеSQLException
Опять такиПроверить исключение, которыйобязательныйМы должны выполнять обработку исключений при использовании его методов. Итак, как транзакция Spring решает эту проблему. Давайте посмотримdoCommit
метод:
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
con.commit();
}
catch (SQLException ex) {
// 异常转换
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
Connection
изcommit
метод выдает проверенное исключениеSQLException
, в блоке catchSQLException
будет преобразован вTransactionSystemException
бросать, покаTransactionSystemException
является непроверенным исключением. поставивПреобразование проверенных исключений в непроверенные исключения, что позволяет нам решить, перехватывать исключение или нет, без принудительной обработки исключений.
В транзакции Spring соответствующие исключения определены почти для всех ошибок в базе данных, а различные API-интерфейсы исключений, такие как JDBC, Hibernate и MyBatis, унифицированы. Это помогает нам использовать единый интерфейс API исключений при обработке исключений, не заботясь о конкретных технологиях доступа к данным.
Резюме: транзакции Spring избегают обязательной обработки исключений посредством преобразования исключений.
2.3 Смешанный код бизнес-процессинга и код управления транзакциями
В Разделе 2.1 приведен метод написания с использованием Spring API транзакций, то есть программное управление транзакциями, но не решена проблема «смешанного кода бизнес-обработки и кода управления транзакциями». В настоящее время вы можете использовать Spring AOP, чтобы удалить сквозную задачу кода управления транзакциями из кода, то естьДекларативное управление транзакциями. Возьмите метод аннотации в качестве примера, пометив метод@Transaction
Аннотация, которая обеспечит управление транзакциями для этого метода. Его принцип показан на следующем рисунке:
Весенние сделки будут@Transaction
Класс аннотированного метода генерирует объект динамического прокси-класса с расширенными возможностями АОП и добавляет его в цепочку перехвата, которая вызывает целевой метод.TransactionInterceptor
Объемное увеличение для достижения управления транзакциями.
Давайте посмотримTransactionInterceptor
Конкретная реализация вinvoke
метод вызоветinvokeWithinTransaction
метод управления транзакциями следующим образом:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// 查询目标方法事务属性、确定事务管理器、构造连接点标识(用于确认事务名称)
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 通过回调执行目标方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 目标方法执行抛出异常,根据异常类型执行事务提交或者回滚操作
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清理当前线程事务信息
cleanupTransactionInfo(txInfo);
}
// 目标方法执行成功,提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
} else {
// 带回调的事务执行处理,一般用于编程式事务
// ...
}
}
Такие операции, как создание транзакций, обработка исключений и фиксация транзакций, добавляются до и после вызова целевого метода. Это избавляет нас от необходимости писать код управления транзакциями, просто@Transaction
Свойство указывает метаинформацию, связанную с транзакцией.
Описание: Транзакции Spring обеспечивают декларативные транзакции через АОП для разделения кода бизнес-процессов и кода управления транзакциями.
3. В чем проблема
Транзакции Spring решают для нас три проблемы, перечисленные в первом разделе, но также приносят некоторые новые проблемы.
3.1 Аннулирование непубличных методов
@Transactional
Он повлияет только на методы, отмеченные на общедоступном уровне, и не повлияет на методы, не являющиеся общедоступными. Это связано с тем, что Spring AOP не поддерживает перехват частных и защищенных методов. В принципе, динамические прокси реализуются через интерфейсы, поэтому, естественно, приватные и защищенные методы поддерживаться не могут. CGLIB реализован через наследование, что на самом деле может поддерживать перехват метода protected, но Spring AOP не поддерживает такое использование.Думаю, это ограничение связано с соображением, что метод прокси должен быть общедоступным, и для того, чтобы поддерживать CGLIB и динамический прокси. AspectJ рекомендуется, если вам нужно перехватить методы protected или private.
3.2 Аннулирование самовызова
При вызове напрямую через внутренний метод бина с@Transactional
метод,@Transactional
не получится, например:
public void saveAB(A a, B b)
{
saveA(a);
saveB(b);
}
@Transactional
public void saveA(A a)
{
dao.saveA(a);
}
@Transactional
public void saveB(B b)
{
dao.saveB(b);
}
Вызовите методы saveA и saveB в saveAB, два@Transactional
не удастся. Это связано с тем, что реализация транзакций Spring основана на прокси-классах.Когда метод вызывается напрямую внутри, он не проходит через прокси-объект, а напрямую вызывает метод целевого объекта, который не может быть вызван прокси-объектом.TransactionInterceptor
Обработка перехвата. Решение:
(1) ApplicationContextAware
пройти черезApplicationContextAware
Внедренный контекст получает прокси-объект.
public void saveAB(A a, B b)
{
Test self = (Test) applicationContext.getBean("Test");
self.saveA(a);
self.saveB(b);
}
(2) Аопконтекст
пройти черезAopContext
Получите прокси-объект.
public void saveAB(A a, B b)
{
Test self = (Test)AopContext.currentProxy();
self.saveA(a);
self.saveB(b);
}
(3) @Autowired
пройти через@Autowired
Аннотация вводится в прокси-объект.
@Component
public class Test {
@Autowired
Test self;
public void saveAB(A a, B b)
{
self.saveA(a);
self.saveB(b);
}
// ...
}
(4) Сплит
Разделите методы saveA, saveB на другой класс.
public void saveAB(A a, B b)
{
txOperate.saveA(a);
txOperate.saveB(b);
}
Обе вышеупомянутые проблемы вызваны ограничениями в способе реализации транзакций Spring. Посмотрим еще дваНеправильное использованиеДве легкие ошибки.
3.3 Проверить, что исключения не откатываются по умолчанию
По умолчанию создание непроверенного исключения вызывает откат, а проверенное исключение — нет.
согласно сinvokeWithinTransaction
метод, мы можем знать, что логика обработки исключений находится вcompleteTransactionAfterThrowing
метод, который реализуется следующим образом:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 异常类型为回滚异常,执行事务回滚
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
else {
try {
// 异常类型为非回滚异常,仍然执行事务提交
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}
согласно сrollbackOn
Определяет, является ли исключение исключением отката. ТолькоRuntimeException
а такжеError
экземпляр, то есть непроверенное исключение, или в@Transaction
прошедшийrollbackFor
Транзакция будет отменена, только если используется тип исключения отката, указанный в атрибуте. В противном случае транзакция будет по-прежнему зафиксирована. Поэтому, если вам нужно откатить непроверенное исключение, вам нужно не забыть указатьrollbackFor
свойство, в противном случае оно будет отменено и аннулировано.
3.4 Исключение catch нельзя откатить
В Разделе 3.3 мы говорили, что генерируются только непроверенные исключения.rollbackFor
Исключение, указанное в, может вызвать откат. Если мы перехватим исключение и не бросим его, откат не сработает, что также является распространенной ошибкой в разработке. Например:
@Transactional
public void insert(List<User> users) {
try {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
for (User user : users) {
String insertUserSql = "insert into User (id, name) values (?,?)";
jdbcTemplate.update(insertUserSql, new Object[] { user.getId(),
user.getName() });
}
} catch (Exception e) {
e.printStackTrace();
}
}
здесь из-за улова все живутException
, так и не кинул. Когда при вставке возникает исключение, откат не запускается.
Но в то же время мы также можем использовать этот механизм для обертывания операций с данными, которым не нужно участвовать в транзакциях с помощью try-catch, например, для записи каких-то неважных логов мы можем обернуть их с помощью try-catch, чтобы избежать выбрасывания исключений. , Ошибка записи в журнал влияет на фиксацию транзакции.