一文读懂Spring事务管理器

7次阅读

共计 5072 个字符,预计需要花费 13 分钟才能阅读完成。

为什么需要事务管理器

如果没有事务管理器的话,我们的程序可能是这样:

Connection connection = acquireConnection();
try{int updated = connection.prepareStatement().executeUpdate();
    connection.commit();}catch (Exception e){rollback(connection);
}finally {releaseConnection(connection);
}

也有可能是这样 ” 优雅的事务 ”:

execute(new TxCallback() {
    @Override
    public Object doInTx(Connection var1) {
        //do something...
        return null;
    }
});
public void execute(TxCallback txCallback){Connection connection = acquireConnection();
    try{txCallback.doInTx(connection);
        connection.commit();}catch (Exception e){rollback(connection);
    }finally {releaseConnection(connection);
    }
}

# lambda 版
execute(connection -> {
    //do something...
    return null;
});

但是以上两种方式,针对一些复杂的场景是很不方便的。在实际的业务场景中,往往有比较复杂的业务逻辑,代码冗长,逻辑关联复杂,如果一个大操作中有全是这种代码的话我想开发人员可能会疯把。更不用提定制化的隔离级别,以及嵌套 / 独立事务的处理了。

Spring 事务管理器简介

Spring 作为 Java 最强框架,事务管理也是其核心功能之一。Spring 为事务管理提供了统一的抽象,有以下优点:

  • 跨不同事务 API(例如 Java 事务 API(JTA),JDBC,Hibernate,Java 持久性 API(JPA)和 Java 数据对象(JDO))的一致编程模型。
  • 支持声明式事务管理(注解形式)
  • 与 JTA 之类的复杂事务 API 相比,用于程序化事务管理的 API 更简单
  • 和 Spring 的 Data 层抽象集成方便(比如 Spring – Hibernate/Jdbc/Mybatis/Jpa…)

使用方式

事务,自然是控制业务的,在一个业务流程内,往往希望保证原子性,要么全成功要么全失败。

所以事务一般是加载 @Service 层,一个 Service 内调用了多个操作数据库的操作(比如 Dao),在 Service 结束后事务自动提交,如有异常抛出则事务回滚。

这也是 Spring 事务管理的基本使用原则。

下面贴出具体的使用代码:

注解

在被 Spring 管理的类头上增加 @Transactional 注解,即可对该类下的所有方法开启事务管理。事务开启后,方法内的操作无需手动开启 / 提交 / 回滚事务,一切交给 Spring 管理即可。

@Service
@Transactional
public class TxTestService{
    
    @Autowired
    private OrderRepo orderRepo;

    public void submit(Order order){orderRepo.save(order);
    }
}

也可以只在方法上配置,方法配置的优先级是大于类的

@Service
public class TxTestService{
    
    @Autowired
    private OrderRepo orderRepo;


    @Transactional
    public void submit(Order order){orderRepo.save(order);
    }
}

XML

XML 的配置方式较为古老,此处就不贴代码了,如有需要自行搜索

隔离级别

事务隔离级别是数据库最重要的特性之一,他保证了脏读 / 幻读等问题不会发生。作为一个事务管理框架自然也是支持此配置的,在 @Transactional 注解中有一个 isolation 配置,可以很方便的配置各个事务的隔离级别,等同于connection.setTransactionIsolation()

Isolation {DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}

传播行为

可能没有接触过 Spring 的人听到传播行为会奇怪,这是个什么东西。

其实这个传播行为和数据库功能无关,只是事务管理器为了处理复杂业务而设计的一个机制。

比如现在有这样一个调用场景,A Service -> B Service -> C Service,但是希望 A / B 在一个事务内,C 是一个独立的事务,同时 C 如果出错,不影响 AB 所在的事务。

此时,就可以通过传播行为来处理;将 C Service 的事务配置为 @Transactional(propagation = Propagation.REQUIRES_NEW) 即可

Spring 支持以下几种传播行为:

REQUIRED

默认策略,优先使用当前事务(及当前线程绑定的事务资源),如果不存在事务,则开启新事务

SUPPORTS

优先使用当前的事务(及当前线程绑定的事务资源),如果不存在事务,则以无事务方式运行

MANDATORY

优先使用当前的事务,如果不存在事务,则抛出异常

REQUIRES_NEW

创建一个新事务,如果存在当前事务,则挂起(Suspend)

NOT_SUPPORTED

以非事务方式执行,如果当前事务存在,则挂起当前事务。

NEVER

以非事务方式执行,如果当前事务存在,则抛出异常

回滚策略

@Transactional 中有 4 个配置回滚策略的属性,分为 Rollback 策略,和 NoRollback 策略

默认情况下,RuntimeException 和 Error 这两种异常会导致事务回滚,普通的 Exception(需要 Catch 的)异常不会回滚。

Rollback

配置需要回滚的异常类

# 异常类 Class
Class<? extends Throwable>[] rollbackFor() default {};
# 异常类 ClassName,可以是 FullName/SimpleName
String[] rollbackForClassName() default {};

NoRollback

针对一些要特殊处理的业务逻辑,比如插一些日志表,或者不重要的业务流程,希望就算出错也不影响事务的提交。

可以通过配置 NoRollbackFor 来实现,让某些异常不影响事务的状态。

# 异常类 Class
Class<? extends Throwable>[] noRollbackFor() default {};
# 异常类 ClassName,可以是 FullName/SimpleName
String[] noRollbackForClassName() default {};

只读控制

设置当时事务的只读标示,等同于connection.setReadOnly()

常见问题

事务没生效

有下列代码,入口为 test 方法,在 testTx 方法中配置了 @Transactional 注解,同时在插入数据后抛出 RuntimeException 异常,但是方法执行后插入的数据并没有回滚,竟然插入成功了

public void test(){testTx();
}

@Transactional
public void testTx(){UrlMappingEntity urlMappingEntity = new UrlMappingEntity();
    urlMappingEntity.setUrl("http://www.baidu.com");
    urlMappingEntity.setExpireIn(777l);
    urlMappingEntity.setCreateTime(new Date());
    urlMappingRepository.save(urlMappingEntity);
    if(true){throw new RuntimeException();
    }
}

这里不生效的原因是因为入口的方法 / 类没有增加 @Transaction 注解,由于 Spring 的事务管理器也是基于 AOP 实现的,不管是 Cglib(ASM)还是 Jdk 的动态代理,本质上也都是子类机制;在同类之间的方法调用会直接调用本类代码,不会执行动态代理曾的代码;所以在这个例子中,由于入口方法 test 没有增加代理注解,所以 textTx 方法上增加的事务注解并不会生效

异步后事务失效

比如在一个事务方法中,开启了子线程操作库,那么此时子线程的事务和主线程事务是不同的。

因为在 Spring 的事务管理器中,事务相关的资源(连接,session,事务状态之类)都是存放在 TransactionSynchronizationManager 中的,通过 ThreadLocal 存放,如果跨线程的话就无法保证一个事务了

# TransactionSynchronizationManager.java
private static final ThreadLocal<Map<Object, Object>> resources =
        new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
        new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
        new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
        new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
        new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
        new NamedThreadLocal<>("Actual transaction active");

事务提交失败

org.springframework.transaction.UnexpectedRollbackException: 
Transaction silently rolled back because it has been marked as rollback-only

这个异常是由于在同一个事务内,多个事务方法之间调用,子方法抛出异常,但又被父方法忽略了导致的。

因为子方法抛出了异常,Spring 事务管理器会将当前事务标为失败状态,准备进行回滚,可是当子方法执行完毕出栈后,父方法又忽略了此异常,待方法执行完毕后正常提交时,事务管理器会检查回滚状态,若有回滚标示则抛出此异常。具体可以参考org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit

示例代码:

A -> B
# A Service(@Transactional):
public void testTx(){urlMappingRepo.deleteById(98l);
    try{txSubService.testSubTx();
    }catch (Exception e){e.printStackTrace();
    }
}

# B Service(@Transactional)
public void testSubTx(){if(true){throw new RuntimeException();
    }
}

参考

  • https://docs.spring.io/spring…
正文完
 0