事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编程式和声明式的两种方式。本篇只说明声明式注解。
只需在方法加上 @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 "成功";
}
所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持以下 7 种事务传播行为:
传播类型 | 描述 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,如果有就加入当前事务中。如果当前方法没有事务,则新建一个事务 |
PROPAGATION_SUPPORTS | 支持当前事务,如果有就加入当前事务中。如果当前方法没有事务,则以非事务方式执行 |
PROPAGATION_MANDATORY | 支持当前事务,如果有就加入当前事务中。如果当前没有事务,则抛出异常 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,就把当前事务挂起。如果当前方法没有事务,就新建事务。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行,如果当前方法存在事务就挂起当前事务;如果当前方法不存在事务,就以非事务方式执行; |
PROPAGATION_NEVER | 以非事务方式执行,如果当前方法存在事务就抛出异常;如果当前方法不存在事务,就以非事务方式执行 |
PROPAGATION_NESTED | 如果当前方法有事务,则在嵌套事务内执行;如果当前方法没有事务,则与PROPAGATION_REQUIRED相同 |
名词解释:
详解 Spring 的事务传播属性以及在写代码的过程中发生嵌套并发生事务失效的场景
再说这些之前,大家先要消除一个问题, Spring 的事务是怎么实现的?
Spring 本身是没有事务的,只有数据库才会有事务,而 Spring 的事务是借助 AOP,通过动态代理的方式,在我们要操作数据库的时候,实际是 Spring 通过动态代理进行功能拓展,在我们的代码操作数据库之前通过数据库客户端打开数据库事务,如果代码执行完毕没有异常信息或者是没有 Spring 要捕获的异常信息时,再通过数据库客户端提交事务,如果有异常信息或者是有 Spring 要捕获的异常信息,再通过数据库客户端程序回滚事务,从而达到控制数据库事务的目的。
比如如下代码,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 的事务拦截器拦截。
总结:
protected、private 修饰的方法上使用 @Transactional 注解,事务是无效
propagation 属性设置为 PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、 PROPAGATION_NEVER 这三种类别时,@Transactional 注解就不会产生效果。
Spring 默认回滚事务分别为抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)和 Error 两种情况,其他异常不会回滚,希望抛出其他异常 Spring 亦能回滚事务,需要指定 rollbackFor 属性
@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);
}
@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 方法的空指针异常。
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。