Spring 对事务的支持
Spring 支持两种事务方式,分别是编程式事务和声明式事务,后者最常见,通常情况下只需要一个 @Transactional
就搞定了(代码侵入性降到了最低),就像这样:
@Transactional
public void savePosts(PostsParam postsParam) {
// 保存文章
save(posts);
// 处理标签
insertOrUpdateTag(postsParam, posts);
}
编程式事务
编程式事务是指将事务管理代码嵌入到业务代码中,来控制事务的提交和回滚。
使用 TransactionTemplate 来管理事务:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
就编程式事务管理而言,Spring 更推荐使用 TransactionTemplate。
在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。
声明式事务
声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。
当然了,要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中最关键最核心的技术之一AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的
事务管理模型
Spring 将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:
public interface TransactionManager {
}
该接口有两个子接口,分别是编程式事务接口 ReactiveTransactionManager 和声明式事务接口 PlatformTransactionManager。我们来重点说说 PlatformTransactionManager,该接口定义了 3 个接口方法:
interface PlatformTransactionManager extends TransactionManager{
// 根据事务定义获取事务状态
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 事务回滚
void rollback(TransactionStatus status) throws TransactionException;
}
通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
参数 TransactionDefinition 和 @Transactional 注解是对应的,比如说 @Transactional 注解中定义的事务传播行为、隔离级别、事务超时时间、事务是否只读等属性,在 TransactionDefinition 都可以找得到。
返回类型 TransactionStatus 主要用来存储当前事务的一些状态和数据,比如说事务资源(connection)、回滚状态等。
TransactionDefinition.java:
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;
}
}
Transactional.java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
}
- @Transactional 注解中的 propagation 对应 TransactionDefinition 中的 getPropagationBehavior,默认值为
Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)
。 - @Transactional 注解中的 isolation 对应 TransactionDefinition 中的 getIsolationLevel,默认值为
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)
。 - @Transactional 注解中的 timeout 对应 TransactionDefinition 中的 getTimeout,默认值为TransactionDefinition.TIMEOUT_DEFAULT。
- @Transactional 注解中的 readOnly 对应 TransactionDefinition 中的 isReadOnly,默认值为 false。
事务传播行为
当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}
TransactionDefinition 一共定义了 7 种事务传播行为:
1、PROPAGATION_REQUIRED(默认)
- 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。
Class A { @Transactional(propagation=Propagation.PROPAGATION_REQUIRED) public void aMethod { //do something B b = new B(); b.bMethod(); } } Class B { @Transactional(propagation=Propagation.PROPAGATION_REQUIRED) public void bMethod { //do something } }
这个传播行为也最好理解,aMethod 调用了 bMethod,只要其中一个方法回滚,整个事务均回滚。
2、PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。
Class A {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
如果 aMethod()发生异常回滚,bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。但是,如果 bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚。
3、PROPAGATION_NESTED
如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。
4、PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
5、PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
6、PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
7、PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
事务隔离级别
- ISOLATION_DEFAULT,使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。
- ISOLATION_READ_UNCOMMITTED,最低的隔离级别,可能会出现脏读、幻读或者不可重复读。
- ISOLATION_READ_COMMITTED,允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生
- ISOLATION_REPEATABLE_READ,对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生
- ISOLATION_SERIALIZABLE,最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能
事务的超时时间
事务超时,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。
假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。
事务的只读属性
如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。
为什么一个查询操作还要启用事务支持呢?
这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。
那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。
事务的回滚策略
默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
如果你想要回滚特定的异常类型的话,可以这样设置:
@Transactional(rollbackFor= MyException.class)
关于 Spring Boot 对事务的支持
@Transactional 的作用范围
- 类上,表明类中所有 public 方法都启用事务
- 方法上,最常用的一种
- 接口上,不推荐使用
@Transactional的常用配置参数
@Transactional 注解源码中定义了很多属性,但大多数时候,我都是采用默认配置,当然了,如果需要自定义的话,前面也都说明过了。
@Transactional 的使用注意事项总结
- 要在 public 方法上使用,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
- 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效。