一、基本介绍
事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编程式和声明式的两种方式。本篇只说明声明式注解。
1、在 spring 项目中, @Transactional 注解默认会回滚运行时异常及其子类,其它范围之外的异常 Spring 不会帮我们去回滚数据(如果也想要回滚,在方法或者类加上@Transactional(rollbackFor = Exception.class) 即可)。异常继承体系如下图
- Throwable 是最顶层的父类,有 Error 和 Exception 两个子类。
- Error:表示严重的错误(如OOM等)
- Exception 可以分为运行时异常( RuntimeException 及其子类)和非运行时异常( Exception 的子类中,除了 RuntimeException 及其子类之外的类)。
- 非运行时异常是检查异常( checked exceptions ),一定要 try catch,因为这类异常是可预料的,编译阶段就检查的出来。如果不抛出异常,该行代码是会报错的,项目也会启动不起来。
- Error 和运行时异常是非检查异常( unchecked exceptions ),不需要 try catch,因为这类异常是不可预料的,编译阶段不会检查。
2、@Transactional 注解只能应用到 public 方法或者类上才有效
二、简单的使用方法
只需在方法加上 @Transactional 注解就可以了。
如下有一个保存数据的方法,加入 @transactional 注解,抛出异常之后,事务会自动回滚,数据不会插入到数据库中。
@Override
@Transactional
public String save(ProductModuleConfig productModuleConfig){
productModuleConfigDao.insert(productModuleConfig);
if (true) {
throw new RuntimeException("save方法运行时异常");
}
return "成功";
}
我们可以从控制台日志可以看出这些信息:
该事务没有提交 commit,因为遇到 RuntimeException 异常该事务进行了回滚,数据库中也没有该条数据。
再看一个简单的使用方法:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public String save(ProductModuleConfig productModuleConfig){
productModuleConfigDao.insert(productModuleConfig);
try {
String a = null;
boolean equals = a.equals("2");
} catch (Exception e) {
e.printStackTrace();
}
return "成功";
}
- save 方法无 @Transactional 注解
- 空指针异常没有被 try catch:插入数据库操作成功
- 空指针异常被 try catch:插入数据库操作成功
- save 方法有 @Transactional 注解
- 空指针异常没有被 try catch:插入数据库操作失败,回滚成功
- 空指针异常被 try catch:插入数据库操作成功,回滚失败
三、@Transactional 注解的属性介绍
事务的传播属性(propagation 属性默认值为 Propagation.REQUIRED)
所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持以下 7 种事务传播行为:
名词解释:
- 当天事务挂起:需要新事物时,将现有的 connection1保存起来(它还有尚未提交的事务),然后创建 connection2,connection2 提交、回滚、关闭完毕后,再把 connection1取出来,完成提交、回滚、关闭等动作,保存 connection1 的动作称之为事务挂起。
详解 Spring 的事务传播属性以及在写代码的过程中发生嵌套并发生事务失效的场景
再说这些之前,大家先要消除一个问题, Spring 的事务是怎么实现的?
Spring 本身是没有事务的,只有数据库才会有事务,而 Spring 的事务是借助 AOP,通过动态代理的方式,在我们要操作数据库的时候,实际是 Spring 通过动态代理进行功能拓展,在我们的代码操作数据库之前通过数据库客户端打开数据库事务,如果代码执行完毕没有异常信息或者是没有 Spring 要捕获的异常信息时,再通过数据库客户端提交事务,如果有异常信息或者是有 Spring 要捕获的异常信息,再通过数据库客户端程序回滚事务,从而达到控制数据库事务的目的。
四、@Transactional 注解的一些代码 demo
比如如下代码,save 方法首先调用了 method1 方法,然后 save 方法抛出了异常,就会导致事务回滚,如下两条数据都不会插入数据库。可从控制台日志信息可以看出,没有提交(commit)事务,直接回滚掉了。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(ProductModuleConfig productModuleConfig){
method1();
productModuleConfigDao.insert(productModuleConfig);
if (true) {
throw new RuntimeException("save方法运行时异常");
}
return "成功";
}
public void method1() {
ProductModuleConfig productModuleConfig = new ProductModuleConfig();
productModuleConfig.setId(UUID.randomUUID().toString());
productModuleConfig.setName("哈哈哈哈2");
productModuleConfigDao.insert(productModuleConfig);
}
现在有如下需求,就算 save 方法的后面抛异常了,也不能影响 method1 方法的数据插入。或许很多人的想法如下,给 method1 方法加入一个新的事务,这样 method1 就会在这个新的事务中执行,原来的事务不会影响到新的事务。比如 method1 方法上面再加入注解 @Transactional ,设置 propagation 属性为 Propagation.REQUIRES_NEW,代码如下:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(ProductModuleConfig productModuleConfig){
method1();
productModuleConfigDao.insert(productModuleConfig);
if (true) {
throw new RuntimeException("save方法运行时异常");
}
return "成功";
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void method1() {
ProductModuleConfig productModuleConfig = new ProductModuleConfig();
productModuleConfig.setId(UUID.randomUUID().toString());
productModuleConfig.setName("哈哈哈哈2");
productModuleConfigDao.insert(productModuleConfig);
}
运行之后,发现数据还是没有插入数据库中。怎么回事,我们先看一下控制台日志打印信息。从日志内容可以看出,其实两个方法都是处于同一个事务中,method1 方法并没有创建一个新的事务。
大概意思:在默认的代理模式下,只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。 在同一个类中的两个方法直接调用,是不会被 Spring 的事务拦截器拦截,就像上面的 save 方法直接调用了同一个类中的 method1 方法,method1 方法不会被 Spring 的事务拦截器拦截,也就是说 method1 方法上的注解是失效的,根本没起作用。
用一个示意图加深一下印象:
看上边的示意图你一定会明白了吧,原因还是因为代理的时候,直接把有事务的方法包在了有事务的代理方法里面了,不管 method2方法是否有 @Transactional 注解,都会随着 method1() 的事务属性决定,如果 method1() 回滚,必然会导致 method2() 也会回滚。
为了解决这个问题,我们可以新建一个类:
@Service
public class OtherServiceImpl implements OtherService {
@Resource
private ProductModuleConfigDao productModuleConfigDao;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void method() {
ProductModuleConfig productModuleConfig = new ProductModuleConfig();
productModuleConfig.setId(UUID.randomUUID().toString());
productModuleConfig.setName("哈哈哈哈3");
productModuleConfigDao.insert(productModuleConfig);
}
}
然后在 save 方法中调用 otherService.method1 方法
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(ProductModuleConfig productModuleConfig){
otherService.method();
productModuleConfigDao.insert(productModuleConfig);
if (true) {
throw new RuntimeException("save方法运行时异常");
}
return "成功";
}
这下,otherService.method 方法的数据插入成功,事务提交了。save 方法的数据未插入,事务回滚了。继续看一下日志内容:
从日志可以看出,首先创建了 save 方法的事务,由于 otherService.method 方法的 @transactional 的 propagation 属性为 Propagation.REQUIRES_NEW(新建事务,如果当前存在事务,就把当前事务挂起。如果当前方法没有事务,就新建事务),所以接着暂停了 save 方法的事务,重新创建了 otherService.method 方法的事务,接着 otherService.method 方法的事务提交,method方法数据保存成功。接着 save 方法事务开始运行碰到错误将其插入的数据进行回滚,但是method方法插入的数据不会回滚。这就印证了只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。
总结:
- 在同一个类中事务嵌套的话,最终的结果应该是取决于最外层的方法事务的传播特性。
- 如果是不同的类的事务嵌套的话,外层的方法按照外层的事务传播属性执行,内层的传播属性按照内层的传播属性的特点去运行。
五、@Transactional 注解失效场景
1、@Transactional 注解应用在非 public 修饰的方法上,导致注解失效
protected、private 修饰的方法上使用 @Transactional 注解,事务是无效
2、propagation 设置错误,导致注解失败
propagation 属性设置为 PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、 PROPAGATION_NEVER 这三种类别时,@Transactional 注解就不会产生效果。
3、rollbackFor 设置错误,@Transactional 注解失败
Spring 默认回滚事务分别为抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)和 Error 两种情况,其他异常不会回滚,希望抛出其他异常 Spring 亦能回滚事务,需要指定 rollbackFor 属性
4、方法之间的互相调用导致 @Transactional 失效(在同一个 Service 中)
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(ProductModuleConfig productModuleConfig){
method1();
productModuleConfigDao.insert(productModuleConfig);
if (true) {
throw new RuntimeException("save方法运行时异常");
}
return "成功";
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void method1() {
ProductModuleConfig productModuleConfig = new ProductModuleConfig();
productModuleConfig.setId(UUID.randomUUID().toString());
productModuleConfig.setName("哈哈哈哈2");
productModuleConfigDao.insert(productModuleConfig);
}
5、异常被 catch 捕获导致 @Transactional 注解失效
@Transactional(propagation = Propagation.REQUIRED)
@Override
public String save(ProductModuleConfig productModuleConfig){
try {
productModuleConfigDao.insert(productModuleConfig);
method2();
} catch (Exception e) {
e.printStackTrace();
}
return "成功";
}
public void method2(){
String a = null;
boolean equals = a.equals("2");
}
method2 方法是会报空指针异常,而 save 方法对其进行了 try catch 了method2 方法的异常,那 save 方法的事务就不能正常回滚,数据还是会插入到数据库中的,最终会报 method2 方法的空指针异常。
6、数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。