Spring 针对 Java Transaction API(JTA)、JDBC、Hibernate 和 Java Persistence API(JPA)等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置机制,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。
事务管理抽象
Spring 事务管理的抽象层主要包括 3 个接口:PlatformTransactionManager、TransactionDefinition 和 TransactionStatus。TransactionDefinition 用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性。PlatformTransactionManager 根据定义的事务属性配置信息创建事务,并用 TransactionStatus 描述这个激活事务的状态。
1. TransactionDefinition
public interface TransactionDefinition {
// 事务传播行为
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
// 事务隔离级别
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
// 事务超时时间,使用底层事务系统的默认超时
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
// 事务是否只读
default boolean isReadOnly() {
return false;
}
}
事务隔离级别:
当前事务和其他事务的隔离程度,在 TransactionDefinition 接口中定义了 5 种事务隔离级别。
ISOLATION_DEFAULT(默认) | 使用数据库默认的隔离级别 |
---|---|
ISOLATION_READ_UNCOMMITTED | 读未提交。最低的隔离级别,允许读取尚未提交的数据变更。可能发生脏读,不可重复读和幻读。 |
ISOLATION_READ_COMMITTED | 读已提交。该级别仅禁止事务读取其中未提交更改的行。可以防止脏读,但可能发生不可重复读和幻读。 |
ISOLATION_REPEATABLE_READ | 可重复读。该级别禁止事务读取其中未提交更改的行,并且对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。可以防止脏读和不可重复读,但可能发生幻读。 |
ISOLATION_SERIALIZABLE | 序列化。最高的隔离级别,可以防止脏读,不可重复读和幻读。 |
事务传播行为:
当我们调用一个基于 Spring 的接口方法时,该方法可能会在内部调用其他的方法以共同完成一个完整的业务操作,因此就会产生方法嵌套调用的情况。Spring 通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标方法中。在 Spring 规定了 7 种类型的事务传播行为。
PROPAGATION_REQUIRED(默认) | 当前方法必须运行在事务中。如果当前存在事务,则加入到这个事务中。如果没有事务,则新建一个事务。 |
---|---|
PROPAGATION_SUPPORTS | 当前方法不是必须运行在事务中。如果当前存在事务,则加入当前事务。如果没有事务,则以非事务方式执行。 |
PROPAGATION_MANDATORY | 当前方法必须在事务中运行。如果当前存在事务,则加入当前事务,如果没有事务,则抛出异常。 |
PROPAGATION_REQUIRES_NEW | 当前方法必须运行在它自己的事务中。新建一个事务。如果存在当前事务,在该方法执行期间,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 当前方法不应该运行在事务中。如果存在当前事务,在方法运行期间,把当前事务挂起。 |
PROPAGATION_NEVER | 当前方法不应该运行在事务中。如果存在当前事务,则抛出异常。 |
PROPAGATION_NESTED | 如果存在当前事务,那么方法将在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独提交或回滚。如果没有事务,则新建一个事务。每个 NESTED 事务执行前,会将当前操作保存下来,叫做 savepoint(保存点)。NESTED 事务在外部事务提交以后自己才会提交,如果当前 NESTED 事务执行失败,则回滚到之前的保存点。 |
事务超时:
表示事务在超时前能运行多久,超过时间后,事务将被回滚。
只读状态:
只读事务不修改任何数据,试图在只读事务中更改数据将引发异常。
2. PlatformTransactionManager
public interface PlatformTransactionManager extends TransactionManager {
// 根据事务定义信息从事务环境中返回一个已存在的事务,或创建一个新的事务
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
// 根据事务的状态提交事务。如果事务状态已被标识为rollback-only,则该方法将执行一个回滚事务的操作
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务,当commit()方法抛出异常时,该方法会被隐式调用
void rollback(TransactionStatus status) throws TransactionException;
}
Spring 将事务管理委托给底层具体的持久化实现框架来完成。因此,Spring 为不同的持久化框架提供了不同的 PlatformTransactionManager 接口的实现类。如果使用 Spring JDBC 或 MyBatis,由于它们都是基于数据源的 Connection 访问数据库的,所以可以使用 DataSourceTransactionManager 子类。只要在 Spring 中进行如下配置即可:
@Configuration
public class Config {
// 数据源
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
// 事务管理器
@Bean
public TransactionManager transactionManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3. TransactionStatus
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
// 判断当前事务是否在内部创建了一个保存点,该保存点是为了支持 Spring 的嵌套事务而创建的
boolean hasSavepoint();
}
public interface TransactionExecution {
// 判断当前事务是否是一个新的事务
boolean isNewTransaction();
// 设置事务回滚标识
void setRollbackOnly();
// 判断当前事务是否已经被标识为 rollback-only
boolean isRollbackOnly();
// 判断当前事务是否已经结束
boolean isCompleted();
}
TransactionStatus 代表一个事务的具体运行状态。事务管理器可以通过该接口获取事务运行期的状态信息,也可以通过该接口间接地回滚事务,它相比于在抛出异常时回滚事务的方式更具有可控性。比如我们想在方法内部进行异常处理,又想让事务在异常时进行回滚,此时就可以手动设置 RollbackOnly 标识:
@Transactional
public void createUserRight1(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
@Transactiona 注解配置
Spring 提供了基于注解的事务配置,即通过 @Transactional 对需要事务增强的 Bean 接口、实现类或方法进行标注。在容器中配置基于注解的事务增强驱动,即可启用基于注解的声明式事务。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 事务管理器bean名称
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
// 事务传播行为
Propagation propagation() default Propagation.REQUIRED;
// 事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务超时时间
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
// 事务读写性
boolean readOnly() default false;
// 定义一组异常类,当事务执行时遇到此异常或其子类异常则进行回滚,默认只回滚RuntimeException及其子类异常
Class<? extends Throwable>[] rollbackFor() default {};
// 定义一组异常类的名称,可以是全限定名的子字符串,遇到时进行回滚
String[] rollbackForClassName() default {};
// 定义一组异常类,当事务执行时遇到此异常或其子类异常不进行回滚
Class<? extends Throwable>[] noRollbackFor() default {};
// 定义一组异常类的名称,当事务执行时遇到此异常或其子类异常不进行回滚
String[] noRollbackForClassName() default {};
}
@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。但 Spring 建议在业务实现类上使用 @Transactional 注解。方法处的注解会覆盖类定义处的注解,如果有些方法需要使用特殊的事务属性,则可以在类注解的基础上提供方法注解。
@Transactional 注解是通过 AOP 实现了事务的处理。可以理解为使用了 try…catch… 来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 块里面我们可以设置事务回滚,如果没有异常则直接提交事务。这里的“一定条件”主要包括两点:
- 只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。
- 默认情况下,出现 RuntimeException(非受检异常)或 Error 时,Spring 才会回滚事务。
如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。
@Transactional 生效原则
1)必须标注在 public 方法上
当 @Transactional 注解修饰在 private 方法上时,是不生效的。除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。 因为 Spring 默认是通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,自然也无法动态增强事务处理逻辑。
为什么要强调是 public?
首先 JDK 动态代理肯定是不行的,因为需要实现接口方法,只能是 public。理论上 CGLIB 方式的代理是可以代理 protected 方法的,因为子类可以访问到父类的 protected 方法,不过如果支持,那么意味着事务可能会因为切换代理实现方式表现不同,大大增加出现 Bug 的可能性,所以为了一致性 Spring 考虑只支持 public。
2)必须通过代理过的类从外部调用目标方法才能生效
Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象。如果是同一个类中的方法调用,为了保证调用的是代理后的对象,不要通过 this 自调用,而是注入一个 self 的 Bean,然后再通过 self 实例调用标记有 @Transactional 注解的方法,因为 self 是由 Spring 通过 CGLIB 方式增强过的类。
对于基于 JDK(接口)代理的 AOP 事务增强来说,由于接口方法都必须是 public 的,这就要求实现类的实现方法也必须是 public 的,同时不能用 static 修饰符。所以,可以实施接口动态代理的方法只能是使用 public 或 public final 修饰符的方法,其他方法不可能被动态代理,相应地也就不能实施 AOP 增强。基于 CGLib 字节码动态代理的方案是通过扩展被增强类,动态创建其子类的方式进行 AOP 增强织入的。由于 final、static、private 修饰符的方法都不能被子类覆盖,相应地这些方法将无法实施 AOP 增强。
这些不能被 Spring 事务增强的特殊方法并非就不能工作在事务环境下,只要它们被外层的事务方法调用了,由于 Spring 事务管理的传播级别,内部方法也可以工作在外部方法所启动的事务上下文中。