事务特性(ACID)
- 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用
- 一致性: 执行事务前后,数据保持一致
- 隔离性: 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
spring 事务管理接口介绍
Spring 框架中,事务管理相关最重要的 3 个接口如下:
- PlatformTransactionManager: (平台)事务管理器,Spring 事务策略的核心
- TransactionDefinition: 事务属性(事务隔离级别、传播行为、超时、只读、回滚规则)
- TransactionStatus: 事务运行状态
我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinition 和 TransactionStatus 这两个接口可以看作是事务的描述。
PlatformTransactionManager 会根据 TransactionDefinition 的定义(比如事务超时时间、隔离界别、传播行为等)来进行事务管理 ,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态(比如是否新事务、是否可以回滚)
一、PlatformTransactionManager(事务管理)
spring 事务管理接口,通过这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了
PlatformTransactionManager 接口中定义了三个方法:
public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
二、TransactionDefinition(事务属性)
用于描述事务隔离级别、传播行为、超时、只读、回滚规则
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
//返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
@Nullable
String getName();
}
三、TransactionStatus(事务状态)
TransactionStatus 接口用来记录事务的状态,该接口定义了一组方法用来获取或判断事务的相应状态信息
PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象
TransactionStatus 接口接口内容如下:
public interface TransactionStatus{
// 是否是新的事物
boolean isNewTransaction();
// 是否有恢复点
boolean hasSavepoint();
// 设置为只回滚
void setRollbackOnly();
// 是否为只回滚
boolean isRollbackOnly();
// 是否已完成
boolean isCompleted;
}
spring 事务属性
事务传播行为
一、简介
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
事务传播行为是为了解决业务层方法之间互相调用的事务问题
spring 定义了 7 中事务传播行为,其含义如下:
事务行为 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务 |
PROPAGATION_SUPPORTS | 当前方法不需要事务上下文,但若存在当前事务,则该方法会加入当前事务 |
PROPAGATION_MANDATORY | 该方法必须在事务中运行,若当前事物不存在,则抛出一个异常:IllegalTransactionStateException("Transaction propagation ‘mandatory’ but no existing transaction found") |
PROPAGATION_REQUIRES_NEW | 当前方法必须运行在它自己的事务中。一个新的事务将被启动。若存在当前事务,则该方法执行期间,当前事务挂起。若用 JTATransactionManager 的话,则需访问 TransactionManager。内层事务和外层事务相互独立,互不影响 |
PROPAGATION_NOT_SUPPORTED | 该方法以非事务方式运行。若存在当前事务,则在该方法运行期间,当前事务挂起。若用 JTATransactionManager 的话,则需访问 TransactionManager |
PROPAGATION_NEVER | 该方法不应该运行在事务上下文,若当前有一个事务正在运行,则抛出异常 |
PROPAGATION_NESTED | 若当前已存在一个事务,则该方法将会在嵌套事务中运行。若没有活动事务, 则按 PROPAGATION_REQUIRED 执行。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。嵌套事务开始执行时, 它将取得一个 savepoint。若嵌套事务失败, 则回滚到此 savepoint。嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交 |
二、示例设计
示例设计中,我们主要分为两部分:同一类中事务方法的传播行为以及不同类中事务方法的传播行为。我们以表中的情况进行组合,模拟事务方法的传播行为
function a(外层事务方法) | function b(内层事务方法) |
---|---|
外层事务正常执行,不捕获内层事务异常 | 内层事务正常执行 |
外层事务正常执行,捕获内层事务异常 | 内层事务执行异常,捕获异常 |
外层事务执行异常,抛出异常 | 内层事务执行异常,抛出异常 |
外层事务执行异常,捕获异常 | 内层非事务 |
2.1 同一类中事务方法的传播行
2.2 不同类中事务方法的传播行为
2.2.1 内层方法没有声明事务
场景:外层 PROPAGATION_REQUIRED,内层没有事务
结果:正常执行
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
serviceB.innerFunction();
}
public void innerFunction() {
// todo database operate
}
场景:外层 PROPAGATION_REQUIRED,内层抛出异常,外层没有捕获异常
结果:全部回滚
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
serviceB.innerFunction();
}
public void innerFunction() {
// todo database operate
throw new RuntimeException("innerFunction RuntimeException");
}
场景:外层 PROPAGATION_REQUIRED,内层抛出异常,外层捕获异常
结果:正常执行,不回滚
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
try {
serviceB.innerFunction();
} catch (Exception e) {
e.printStackTrace();
}
}
public void innerFunction() {
// todo database operate
throw new RuntimeException("innerFunction RuntimeException");
}
2.2.2 PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的 @Transactional 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:
- 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰
- 如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚
场景:内层 PROPAGATION_REQUIRED,并抛出异常,外层事务不捕获异常
结果:全部回滚
原因:外层事务方法没有捕获内层事务方法抛出的异常,因此进行回滚操作
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
serviceB.innerFunction();
}
@Transactional(rollbackFor = Exception.class)
public void innerFunction() {
// todo database operate
throw new RuntimeException("innerFunction RuntimeException");
}
场景:内层 PROPAGATION_REQUIRED,并抛出异常,外层事务捕获异常
结果:全部回滚
原因:当内层事务异常的情况下,如果是 PROPAGATION_REQUIRED,正常来讲是需要回滚的,但是 spring 只给内层事务做了一个 rollback 的标记,当内层事务抛出的异常被外层捕获时,外层事务正常执行,但在最后提交的时候发现,内层事务被标记为 rollbck,所以就会抛出 UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
。
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
try {
serviceB.innerFunction();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(rollbackFor = Exception.class)
public void innerFunction() {
// todo database operate
throw new RuntimeException("innerFunction RuntimeException");
}
解决 UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
的方案有两个:
-
内层事务方法捕获自己抛出的异常
@Transactional(rollbackFor = Exception.class) public void innerFunction() { try { // todo database operate throw new RuntimeException("innerFunction RuntimeException"); } catch (RuntimeException e) { e.printStackTrace(); } }
-
将内层事务传播行为改为 PROPAGATION_REQUIRES_NEW,详情见 2.2.3 Propagation.REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void innerFunction() { // todo database operate throw new RuntimeException("innerFunction RuntimeException"); }
2.2.3 PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰
场景:内层 PROPAGATION_REQUIRES_NEW,并抛出异常,外层事务不捕获异常
结果:外层事务不回滚,内层事务回滚
原因:内外层事务互不影响,内层事务的回滚不影响外层事务的正常执行
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
serviceB.innerFunction();
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void innerFunction() {
// todo database operate
throw new RuntimeException("innerFunction RuntimeException");
}
场景:内层 PROPAGATION_REQUIRES_NEW,并抛出异常,外层事务捕获异常
结果:外层事务不回滚,内层事务回滚
原因:内外层事务互不影响,内层事务的回滚不影响外层事务的正常执行
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
try {
serviceB.innerFunction();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void innerFunction() {
// todo database operate
throw new RuntimeException("innerFunction RuntimeException");
}
场景:外层事务抛出异常,内层事务正常执行
结果:外层事务回滚,内层事务不回滚
原因:内外层事务互不影响,外层事务的回滚不影响内层事务的正常执行
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
serviceB.innerFunction();
throw new RuntimeException("function RuntimeException");
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void innerFunction() {
// todo database operate
}
场景:外层事务抛出异常并捕获,内层事务正常执行
结果:内外层事务均不回滚
原因:外层事务捕获了 RuntimeException,因此不回滚
@Transactional(rollbackFor = Exception.class)
public void function() {
// todo database operate
try {
serviceB.innerFunction();
throw new RuntimeException("function RuntimeException");
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void innerFunction() {
// todo database operate
}
2.2.4 PROPAGATION_NESTED
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。也就是说:
- 在外部方法未开启事务的情况下 Propagation.NESTED 和 Propagation.REQUIRED 作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务的话,Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务
场景:外层事务方法 function 正常执行,内层事务方法 innerFunction1 执行正常,内存事务方法 innerFunction2 抛出异常
结果:全部回滚
原因:有人可能会问,不应该是内层事务的回滚不影响外层事务执行吗?为什么会全部回滚。原因在于 innerFunction2 抛出 RuntimeException 后,function 没有进行捕获处理,因此该 RuntimeException 出发了 function 的 rollbackFor = {Exception.class} 条件,导致所以操作均回滚。正确的方式为对 innerFunction2 包一层 try-catch
语句,这样就达到内层事务回滚不影响外层事务了
// function 没有捕获 innerFunction2 抛出的异常,因此 function 也会回滚,这是错误的打开方式
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public void function() {
// todo database operate
serviceB.innerFunction1();
serviceB.innerFunction2();
}
// 这才是正确的打开方式
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public void function11() {
// todo database operate
try {
serviceB.innerFunction1();
serviceB.innerFunction2();
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void innerFunction1() {
// todo database operate
}
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void innerFunction2() {
// todo database operate
throw new RuntimeException("innerFunction7 RuntimeException");
}
场景:外层事务方法 function 抛出异常,内层事务方法 innerFunction1 和 innerFunction2 执行正常
结果:全部回滚
原因:外层事务影响内层事务
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public void function() {
// todo database operate
try {
serviceB.innerFunction1();
serviceB.innerFunction2();
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("function RuntimeException");
}
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void innerFunction1() {
// todo database operate
}
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void innerFunction2() {
// todo database operate
}
2.2.5 PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
事务隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
public interface TransactionDefinition {
......
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
......
}
事务隔离界别 | 说明 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别,Oracle 默认采用的 READ_COMMITTED 隔离级别 |
ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 |
ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 |
ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 |
ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别 |
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation;
命令来查看
这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以保证事务的隔离性要求,即达到了 SQL 标准的 SERIALIZABLE(可串行化) 隔离级别
事务超时属性
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1
事务只读属性
public interface TransactionDefinition {
......
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
}
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中
很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?
拿 MySQL 的 innodb 举例子,根据官网 https://dev.mysql.com/doc/refman/5.7/en/innodb-autocommit-commit-rollback.html 描述:
MySQL 默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到 MySQL 服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
但是,如果你给方法加上了 @Transactional 注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。
如果不加 @Transactional,每条 sql 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值
分享一下关于事务只读属性,其他人的解答:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
事务回滚规则
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚
事务失效场景
一、数据库引擎是否支持事务
Mysql 的 MyIsam 引擎不支持事务
二、注解所在的类是否注入 spring 容器中
三、注解所在方法不是 public 或者是 final
这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional
注解,这将被忽略,也不会抛出任何异常。
在 spring 中动态代理分为 JDK 动态代理和 CGLIB 动态代理,JDK 动态代理要求必须实现接口(所以方法必须是public的),但是 CGLIB 动态代理底层则是通过字节码生成被代理类的子类来实现的,这里要求被代理类必须能被继承(public 和 protected),被 final 修饰的方法不能被子类继承,因此 @Transactional
注解无效。但为何 @Transactional
注解不支持 protected 方法呢?
spring 官方文档中有如下说明:
Spring AOP 对 private
和 protect
是不支持的,无论是 JDK 还是 CGLIB,如果要对 protect
方法进行拦截,建议使用 AspectJ
不清楚 Spring 为什么不推荐其 AOP 对 protect 不支持,猜测可能:
- 代理行为本身就是一种三方调用的思想,那么被代理的方法本身应该是公有的
- 为了跟让 CGLIB 和 JDK 保持一致,因为 JDK 基于接口的肯定都是 public 的,而 CGLIB 干嘛搞特殊?
- 待续猜想
四、所用数据源是否加载了事务管理器
五、事务自调用(同一个类中的 A 方法调用 B 方法)
若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务被忽略,不会发生回滚
使用 AOP 代理后的方法调用执行流程,如图所示,可以看到调用者首先调用的是 AOP 代理对象而不是目标对象,首先执行事务切面,事务切面内部通过 TransactionInterceptor 环绕增强进行事务的增强,即进入目标方法之前开启事务,退出目标方法时提交/回滚事务
目标对象内部的自我调用将无法实施切面中的增强,如图所示,this 指向目标对象,因此调用 this.b() 将不会执行 b 事务切面,即不会执行事务增强
六、当方法发生异常时,使用 try-catch 捕获了异常,并且 catch 中没有抛出异常或者手动回滚
事务的回滚是方法发生异常,在 aop 的异常通知中进行拦截回滚。如果方法中捕获了异常,是不会被 aop 的异常通知拦截到的。如果使用 try-catch
捕获异常,需要在catch中抛出一个异常或者在 catch 中通过 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
设置手动回滚
@Transactional 事务注解原理
@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现接口,会使用 CGLIB 动态代理
createAopProxy 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
...
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
} else {
return new JdkDynamicAopProxy(config);
}
}
.......
}
如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务
网友评论