Spring 支持两种事务方式,分别是编程式事务和声明式事务,后者最常见,通常情况下只需要一个
@Transactional
就搞定了(代码侵入性降到了最低)。
一、Spring 支持事务类型
1.2 声明式事务
声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。
要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中最关键最核心的技术之一,AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
声明式事务代码:
@Transactional
public void savePosts(PostsParam postsParam) {
// 保存文章
save(posts);
// 处理标签
insertOrUpdateTag(postsParam, posts);
}
1.2 编程式事务
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。
在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。
Spring 更推荐使用 TransactionTemplate
编程式事务代码:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
// java 8 lambada
transactionTemplate.execute(status->{
});
}
TransactionManager
管理事务:
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
二、事务模型
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。
2.1 事务传播行为
当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}
TransactionDefinition 一共定义了 7 种事务传播行为: 只需要重点看 PROPAGATION_REQUIRED
和 PROPAGATION_REQUIRES_NEW
就可以。
2.1.1 PROPAGATION_REQUIRED
:
@Transactional` 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:
- 如果外部方法没有开启事务的话,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.1.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()同样也会回滚。
2.1.3 PROPAGATION_NESTED
如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。
2.1.4 PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
2.1.5 PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
2.1.6 PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
2.1.7 PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
2.2 事务隔离级别
TransactionDefinition 中一共定义了 5 种事务隔离级别:
- ISOLATION_DEFAULT,使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。
- ISOLATION_READ_UNCOMMITTED,最低的隔离级别,可能会出现脏读、幻读或者不可重复读。
- ISOLATION_READ_COMMITTED,允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。
- ISOLATION_REPEATABLE_READ,对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- ISOLATION_SERIALIZABLE,最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。
2.3 数据库并发问题
2.3.1 幻读
幻读(Phantom Read)是一种数据库并发问题,通常出现在事务隔离级别低于可重复读(如读已提交或未提交的隔离级别)时。在这种情况下,事务在两次查询之间,可能会“读到”另一事务新增或删除的数据行,导致前后查询结果不一致。
举个例子
假设有一个银行系统,管理员需要查询账户余额总和,以了解当前的存款情况。
- 事务A:管理员启动一个事务,执行第一次查询操作,得到所有账户余额的总和为1000元。
- 事务B:在事务A还未结束时,另一个管理员在事务B中创建了一个新账户,并存入200元。
- 事务A:再次查询账户余额总和,这次结果变成了1200元。
在事务A内部,管理员希望每次查询到的数据一致,但因为在查询间隙插入了新账户,导致数据“发生了变化”。这种情况就被称为幻读。
解决办法
为了解决幻读问题,可以将事务隔离级别提高到 可重复读 或 可串行化。在可重复读级别下,数据库通常会使用 MVCC(多版本并发控制)来防止幻读,而在串行化级别下,数据库会通过加锁来保证事务的严格隔离,彻底避免幻读问题。
2.3.2 不可重复读
不可重复读(Non-Repeatable Read)是数据库中的一种并发问题,通常发生在隔离级别低于可重复读(如“读已提交”或“未提交读”)时。它指的是同一事务在两次读取同一数据时,数据内容发生了变化。这种情况通常是因为在两个读取操作之间,另一事务对数据进行了更新或删除。
举个例子
假设有一个购物系统,管理员想要在一笔事务内查询某商品的库存数量。
- 事务A:管理员开启一个事务,第一次查询商品库存为100。
- 事务B:在事务A还未结束时,另一个管理员在事务B中对该商品的库存进行了更新,将库存减少到90。
- 事务A:再次查询该商品库存时,结果变成了90。
在事务A中,管理员希望查询到的商品库存数量是一致的,但因为在两次查询之间,另一个事务修改了库存,所以两次查询结果不一致。这就是不可重复读现象。
在事务A中,管理员希望查询到的商品库存数量是一致的,但因为在两次查询之间,另一个事务修改了库存,所以两次查询结果不一致。这就是不可重复读现象。
解决方法
可以将隔离级别提高到可重复读(Repeatable Read),在这种隔离级别下,数据库可以保证同一事务内多次读取的数据内容一致,从而避免不可重复读问题。
不可重复读 vs. 幻读
- 不可重复读:指同一事务内读取的数据内容发生了变化,通常是因为数据被修改或删除。
- 幻读:指同一事务内多次查询时,数据行的数量发生了变化,通常是因为有新数据插入或数据删除,导致查询结果的行数不一致。
2.4 事务的超时时间
事务超时,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。
假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。
2.5 事务的只读属性
如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。
为什么一个查询操作还要启用事务支持呢?
这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。
那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。
2.6 事务的回滚策略
默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
如果你想要回滚特定的异常类型的话,可以这样设置:
@Transactional(rollbackFor= MyException.class)
三、Spring 事务失效场景
在 Spring 中,事务是通过 @Transactional 注解以及底层的 AOP(Aspect-Oriented Programming)机制来实现的。下面详细介绍其原理、使用方式以及常见的失效场景。
Spring 事务的实现原理:
- AOP 切面实现:
- @Transactional 注解基于 AOP 实现,通过代理对象在方法执行前后切入。
- Spring 为标注了 @Transactional 的方法生成代理对象,并在方法调用前后管理事务。
- 代理对象在方法调用前创建一个事务,在方法执行成功后提交事务,或在方法抛出异常时回滚事务。
- 事务管理器:
- Spring 提供了不同类型的事务管理器,例如 DataSourceTransactionManager 用于关系型数据库,JpaTransactionManager 用于 JPA。
- 事务管理器负责具体事务的开启、提交、回滚等操作。
3.1 方法是 private
或 final
Spring 事务依赖 AOP 代理,而代理不能拦截 private 或 final 方法,因此这些方法上的 @Transactional 注解不会生效。
解决办法:将事务方法定义为 public,并避免使用 final 修饰。
3.2 自调用(self-invocation)
当一个 @Transactional 方法在同一类中被另一个方法直接调用时,不会通过代理对象进行调用,事务无法生效。
解决办法:将事务方法抽取到另一个类中,通过依赖注入调用,或者在当前类中使用代理对象调用。
3.3 非 RuntimeException 异常不回滚
Spring 默认仅在 RuntimeException(如 NullPointerException)或 Error 时回滚事务,而 checked exception(如 IOException)不会触发事务回滚。
解决办法:通过 rollbackFor 属性指定回滚的异常类型,例如 @Transactional(rollbackFor = Exception.class)。
3.4 多线程环境
Spring 事务仅在当前线程内有效,若在一个事务方法中启动新线程,则新线程不受事务控制。
解决办法:避免在事务中创建新线程,或使用异步事务管理。
3.5 嵌套事务与传播行为
某些传播行为(如 Propagation.REQUIRES_NEW)会导致嵌套事务失效,因为它会开启一个新的独立事务,外部事务的回滚不影响内部事务。
解决办法:在确定事务传播行为时,需根据具体场景合理设置传播属性。
3.6 未使用 Spring 管理的类
若在未被 Spring 管理的类中使用 @Transactional,事务不会生效,因为 Spring 无法对该类生成代理。
解决办法:确保 @Transactional 注解用于由 Spring 管理的 Bean(如 @Service 或 @Component)。
3.7 事务超时设置不当
若事务执行时间超过 timeout 配置,Spring 会自动回滚事务。这种场景较常见于执行较长时间的数据库操作。
解决办法:为长时间任务合理设置事务超时,或在配置中将事务管理器的 timeout 调大。
3.8 数据库不支持事务
某些数据库操作或存储引擎不支持事务(如 MySQL 的 MyISAM 引擎),在这些情况下 @Transactional 注解也不会生效。
解决办法:使用支持事务的存储引擎(如 InnoDB)。
checked exception 和 unchecked exception 区别
Checked Exception(已检查异常)
- 定义:编译时被强制检查的异常,通常是继承自 Exception 类(但不是 RuntimeException)。
- 处理要求:方法声明中必须使用 throws 抛出该异常,或者在方法体内使用 try-catch 进行捕获。编译器会检查这些异常的处理。
- 常见场景:通常用于那些无法预知或控制的情况,比如 I/O 操作(文件读取、网络连接)等。
Unchecked Exception(未检查异常)
- 定义:在运行时抛出的异常,不会在编译时强制检查,继承自 RuntimeException。
- 处理要求:编译器不要求必须使用 try-catch 捕获或声明 throws 抛出该异常,程序员可以选择处理或不处理。
- 常见场景:一般用于编码错误(如 NullPointerException、ArrayIndexOutOfBoundsException 等),表示程序员的逻辑错误或疏忽,通常可以通过改进代码逻辑避免。
- 举个例子:
public int divide(int a, int b) {
return a / b; // 当b为0时会抛出 ArithmeticException
}