文章目录
1、@Transactional 注解的参数
在声明事务中,我们只需要和注解 @Transactional 打交道,所以我们有必要来深入理解一下这个注解中的参数配置。
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
// 事务管理器、暂时先忽略它,我们也不会去修改这个参数的值
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
// 事务传播行为
Propagation propagation() default Propagation.REQUIRED;
// 事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务超时时间 -1,为永久不超时, 单位是秒
int timeout() default -1;
// 事务超时时间,可以设置单位,比如 timeoutString = "30s"
String timeoutString() default "";
// 是否只读事务
boolean readOnly() default false;
// 对哪些异常进行回滚
Class<? extends Throwable>[] rollbackFor() default {};
// 对哪些异常进行回滚【异常全限定名】
String[] rollbackForClassName() default {};
// 对哪些异常不回滚
Class<? extends Throwable>[] noRollbackFor() default {};
// 对哪些异常不回滚【异常全限定名】
String[] noRollbackForClassName() default {};
}
rollbackFor和rollbackForClassName的区别,直接来看使用方式。 最好使用rollbackFor 可以在编译的时候就帮我买检查是不是对的。
@Transactional(rollbackFor = Exception.class, rollbackForClassName = {"java.lang.Exception"})
@Transactional 注解的参数虽然多,但绝大部分都很好理解。这里主要是来说两个重要且不好理解的参数propagation
和 isolation
1.1、propagation (事务传播行为)
事务的传播行为是指:当前事务方法被调用的时候,需要做什么样的操作。它的配置如下:
值***(小写方便阅读)*** | 描述 |
---|---|
REQUIRED*(required)*默认值 | 如果当前没有事务,则创建一个新的事务,并将当前方法作为事务的起点。**如果当前已经存在事务,则加入到当前事务中,成为当前事务的一部分。**当前事务的提交和回滚都将影响到该方法。 |
REQUIRES_NEW (requires_new) | 无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起,并启动一个新的事务。当前方法独立于外部事务运行,它有自己的事务边界。 |
SUPPORTS*(supports)* | **如果当前存在事务,则加入到当前事务中,成为当前事务的一部分。**如果当前没有事务,则以非事务方式执行。支持当前事务的执行,但不强制要求存在事务。 |
NOT_SUPPORTED (not_supported) | 以非事务方式执行操作。如果当前存在事务,则将其挂起。该方法在一个没有事务的环境中执行。 |
NEVER*(never)* | 以非事务方式执行操作。如果当前存在事务,则抛出异常,表示不允许在事务中执行该方法。 |
MANDATORY*(mandatory)* | 要求当前存在事务,否则抛出异常。该方法必须在一个已经存在的事务中被调用。 |
NESTED (nested) | 如果当前存在事务,则在嵌套事务中执行。如果当前没有事务,则行为类似于 REQUIRED ,创建一个新的事务。 |
存在事务的时候REQUIRED和NESTED的区别:REQUIRED 是加入当前事务,成为当前事务的一部分,NESTED 是生成嵌套事务,本质上是两个事务。(具体区别下面实践演示)
1.2、isolation(事务隔离级别)
其实就是我们之前学习数据库时候的数据库隔离级别了。
值***(小写方便阅读)*** | 描述 |
---|---|
DEFAULT (default) | 默认的,看当前数据库默认的隔离级别是什么 |
READ_UNCOMMITTED (read_uncommitted) | 读未提交 |
READ_COMMITTED (read_committed) | 读已提交 |
REPEATABLE_READ (repeatable_read) | 可重复读 |
SERIALIZABLE (serializable) | 序列化 |
2、@Thransctionl 使用方式:
-
当我们的方法上面加了@Thransctionl 注解的时候,但是在我们的方法里面直接抛出 throw new Exception(“”) 事,事务注解是不起作用的,控制不了事务的;
-
因为在@Thransctionl 注解中有一个 "rollbackFor"的属性:
Class <? extends Throwable>[]rollbackFor( ) default{}
而rollbackFor是针对什么样的异常回滚呢?
在官方的文档中指出只会对运行异常时异常(RuntimeException)和错误(Error)进行回滚;
当我们抛出Exception,受检异常是不能回滚的;
-
所以当我们在业务代码中要抛出异常时,不能抛出这一类的受检异常,而是运行异常,或者呢在@Thransctionl 注解中声明一下“rollbackFor” 什么样的异常要回
@Transactional(rollbackFor = Exception.class)
-
或者我们抛出一个自定义异常,该自定义异常它又继承RuntimeException 运行时异常
throw new CustomException(“数据有误,需要回滚!”)
这样就不用在方法上面声明这个异常需要回滚了 ;
3、@Thransctionl 使用场景:
-
@Transactional注解 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
-
虽然@Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional注解应该只被应用到 public 方法上,这是由Spring AOP的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
-
需要注意的是,在同一个类中调用被 @Transactional 注解修饰的方法时,事务处理可能会失效,因为 Spring 会在运行时为该类创建一个代理对象,而在同一个类中调用方法时,是不会经过代理对象的。如果需要在同一个类中调用被 @Transactional 注解修饰的方法,可以将该方法抽取到另一个类中,并通过依赖注入的方式调用。也可以先获取到本类的代理对象再调用内部方法。
3.2 函数之间相互调用
关于有@Transactional的函数之间调用,会产生什么情况。这里咱们通过几个例子来说明。
3.2.1 同一个类中函数相互调用
同一个类AClass中,有两个函数aFunction、aInnerFunction。aFunction调用aInnerFunction。而且aFunction函数会被外部调用。
情况1:
aFunction添加了@Transactional注解,aInnerFunction函数没有添加。aInnerFunction抛异常。
public class AClass {
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数
}
private void aInnerFunction() {
//todo: 操作数据B(做了增,删,改 操作)
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数操作的数据都会回滚。
情况2:
两个函数都添加了@Transactional注解。aInnerFunction抛异常。
public class AClass {
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
private void aInnerFunction() {
//todo: 操作数据B(做了增,删,改 操作)
throw new RuntimeException("函数执行有异常!");
}
}
结果:同第一种情况一样,两个函数对数据库操作都会回滚。因为同一个类中函数相互调用的时候,内部函数添加@Transactional注解无效。@Transactional注解只有外部调用才有效。
情况3:
aFunction不添加注解,aInnerFunction添加注解。aInnerFunction抛异常。
public class AClass {
public void aFunction() {
//todo: 数据库操作A(增,删,该)
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数
}
@Transactional(rollbackFor = Exception.class)
protected void aInnerFunction() {
//todo: 操作数据B(做了增,删,改 操作)
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数对数据库的操作都不会回滚。因为内部函数@Transactional注解添加和没添加一样。
情况4:
aFunction添加了@Transactional注解,aInnerFunction函数没有添加。aInnerFunction抛异常,不过在aFunction里面把异常抓出来了。
public class AClass {
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
try {
aInnerFunction(); // 调用内部没有添加@Transactional注解的函数
} catch (Exception e) {
e.printStackTrace();
}
}
private void aInnerFunction() {
//todo: 操作数据B(做了增,删,改 操作)
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数里面的数据库操作都成功。事务回滚的动作发生在当有@Transactional注解函数有对应异常抛出时才会回滚。(当然了要看你添加的@Transactional注解有没有效)。
3.2.1. 不同类中函数相互调用
两个类AClass、BClass。AClass类有aFunction、BClass类有bFunction。AClass类aFunction调用BClass类bFunction。最终会在外部调用AClass类的aFunction。
情况1:
aFunction添加注解,bFunction不添加注解。bFunction抛异常。
@Service()
public class AClass {
private BClass bClass;
@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
bClass.bFunction();
}
}
@Service()
public class BClass {
public void bFunction() {
//todo: 数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数对数据库的操作都回滚了。
情况2:
aFunction、bFunction两个函数都添加注解,bFunction抛异常。
@Service()
public class AClass {
private BClass bClass;
@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
bClass.bFunction();
}
}
@Service()
public class BClass {
@Transactional(rollbackFor = Exception.class)
public void bFunction() {
//todo: 数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数对数据库的操作都回滚了。两个函数里面用的还是同一个事务。这种情况下,你可以认为事务rollback了两次。两个函数都有异常。
情况3:
aFunction、bFunction两个函数都添加注解,bFunction抛异常。aFunction抓出异常。
@Service()
public class AClass {
private BClass bClass;
@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
try {
bClass.bFunction();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service()
public class BClass {
@Transactional(rollbackFor = Exception.class)
public void bFunction() {
//todo: 数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数数据库操作都没成功。而且还抛异常了。org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。看打印出来的解释也很好理解把。咱们也可以这么理解,两个函数用的是同一个事务。bFunction函数抛了异常,调了事务的rollback函数。事务被标记了只能rollback了。程序继续执行,aFunction函数里面把异常给抓出来了,这个时候aFunction函数没有抛出异常,既然你没有异常那事务就需要提交,会调事务的commit函数。而之前已经标记了事务只能rollback-only(以为是同一个事务)。直接就抛异常了,不让调了。
情况4:
aFunction、bFunction两个函数都添加注解,bFunction抛异常。aFunction抓出异常。这里要注意bFunction函数@Transactional注解我们是有变化的,加了一个参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为。表明是一个新的事务。其实咱们情况3就是来解决情况2的问题的。
@Service()
public class AClass {
private BClass bClass;
@Autowired
public void setbClass(BClass bClass) {
this.bClass = bClass;
}
@Transactional(rollbackFor = Exception.class)
public void aFunction() {
//todo: 数据库操作A(增,删,该)
try {
bClass.bFunction();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service()
public class BClass {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void bFunction() {
//todo: 数据库操作A(增,删,该)
throw new RuntimeException("函数执行有异常!");
}
}
结果:bFunction函数里面的操作回滚了,aFunction里面的操作成功了。有了前面情况2的理解。这种情况也很好解释。两个函数不是同一个事务了。
1.3.4 同类内方法调用如何使事务生效
上面示例中,在同一个类中调用被 @Transactional 注解修饰的方法时,事务处理可能会失效,因为 Spring 会在运行时为该类创建一个代理对象,而在同一个类中调用方法时,是不会经过代理对象的。如果需要在同一个类中调用被 @Transactional 注解修饰的方法,可以将该方法抽取到另一个类中,并通过依赖注入的方式调用。也可以先获取到本类的代理对象再调用内部方法。
方法1:
使用 AopContext 类获取代理对象
需要注意的是,使用 AopContext 类获取代理对象可能会带来安全风险,因此默认情况下是禁用的。如果需要启用该功能,可以在配置文件中设置 expose-proxy 属性为 true。例如,在 Spring Boot 中可以通过以下方式设置:spring.aop.proxy-target-class: true
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(User user) {
// 获取当前对象的代理对象
UserService userService = (UserService) AopContext.currentProxy();
// 调用内部方法,启用事务处理
userService.updateUserDetail(user.getUserId(), user.getUserDetail());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUserDetail(Long userId, UserDetail userDetail) {
userDao.updateUserDetail(userId, userDetail);
}
}
方法2:
使用 beanFactory.getBean 类获取代理对象
@Service
public class UserService {
@Autowired
private BeanFactory beanFactory;
@Autowired
private UserDao userDao;
@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(User user) {
// 获取当前对象的代理对象
UserService userService = beanFactory.getBean(UserService.class);
// 调用内部方法,启用事务处理
userService.updateUserDetail(user.getUserId(), user.getUserDetail());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUserDetail(Long userId, UserDetail userDetail) {
userDao.updateUserDetail(userId, userDetail);
}
}
在上面的代码中,updateUser() 方法通过 beanFactory.getBean(UserService.class) 方法获取当前对象的代理对象,然后调用内部方法 updateUserDetail(),从而启用事务处理。需要注意的是,在使用 getBean() 方法获取当前对象的代理对象时,要求该类必须是一个 Spring 管理的 Bean,否则会抛出异常。
总结:
要知道@Transactional注解里面每个属性的含义。@Transactional注解属性就是来控制事务属性的。通过这些属性来生成事务。
要明确我们添加的@Transactional注解会不会起作用。@Transactional注解在外部调用的函数上才有效果,内部调用的函数添加无效,要切记。这是由AOP的特性决定的。
要明确事务的作用范围,有@Transactional的函数调用有@Transactional的函数的时候,进入第二个函数的时候是新的事务,还是沿用之前的事务。稍不注意就会抛UnexpectedRollbackException异常。