平时开发中会遇到需要加事务的情况,常用的是使用注解@Transactional,但是有时发现明明加了,但没有生效,为什么呢?
今天就来讲解几种平时会遇到的事务失效的场景,避免以后踩坑。点了在看就是学会了,哈哈哈。
一、事务失效场景
1、方法的访问类型不是 public
@Transactional
private void save() {
User user = getUser();
userMapper.insert(user);
this.teachSave();
}
我们可以看到 save 方法使用了 private,这是不太容易发现的场景。
spring 要求代理的方法必须是 public 修饰,通过以下源码就可以发现,要求是 public,不是 public 的返回 null,也就是不支持事务。
2、方法内部调用
比如有两个方法,test1 和 test2,test1 调用 test2,会有以下 4 种场景:
-
test1 有事务,test2 无事务,事务生效
-
test1 有事务,test2 有事务,事务生效
-
test1 无事务,test2 有事务,事务不生效
-
test1 无事务,test2 无事务,事务不生效
2.1、test1 有事务,test2 无事务
@Transactional
public void test1() {
User user = getUser();
userMapper.insert(user);
this.test2();
}
public void test2() {
Teacher t = getTeacher();
teacherMapper.insert(t);
int a = 1 / 0;
}
test1 方法的事务生效时,那么对 user 和 teacher 表的插入操作,同属于一个事务,要么都成功,要么都失败。
2.2、test1 有事务,test2 有事务
@Transactional
public void test1() {
User user = getUser();
userMapper.insert(user);
this.test2();
}
@Transactional
public void test2() {
Teacher t = getTeacher();
teacherMapper.insert(t);
int a = 1 / 0;
}
test1 方法的事务生效时,那么对 user 和 teacher 表的插入操作,test2 方法的事务生不生效,结果都是一样,效果同 2.1,同属于一个事务,要么都成功,要么都失败。
2.3、test1 无事务,test2 有事务
public void test1() {
User user = getUser();
userMapper.insert(user);
this.test2();
}
@Transactional
public void test2() {
Teacher t = getTeacher();
teacherMapper.insert(t);
int a = 1 / 0;
}
对 user 和 teacher 表的插入操作,通过直接调用 this 对象的方法,所以 test2 方法的事务是失效的,不能使用 this.的方式调用。
2.4、test1 无事务,test2 无事务
public void test1() {
User user = getUser();
userMapper.insert(user);
this.test2();
}
public void test2() {
Teacher t = getTeacher();
teacherMapper.insert(t);
int a = 1 / 0;
}
这个场景比较好理解,都没加事务,所以对 user 和 teacher 的操作,没有事务。
通过上面的 4 种场景,可以知道通过 this 调用内部方法,事务是不生效的,那怎样才能让事务生效呢?
- 将方法 test2 放在另一个 serviceB 里,通过 serviceB 调用 test2,这样事务就可以生效
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void test1() {
serviceB.test2();
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void test2() {
addUser();
addTeacher();
}
}
- 注入自己
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;
public void test1() {
serviceA.test2();
}
@Transactional(rollbackFor=Exception.class)
public void test2() {
addUser();
addTeacher();
}
}
注入自己,通过自己调用 test2,事务是生效的
- 使用 AopContext.currentProxy()获取代理对象
@Servcie
public class ServiceA {
public void test1() {
((ServiceA)AopContext.currentProxy()).test2();
}
@Transactional(rollbackFor=Exception.class)
public void test2() {
addUser();
addTeacher();
}
}
在 serviceA 类中使用 AopContext.currentProxy()获取代理对象,事务生效。
3、异常类型错误
@Service
public class ServiceA {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
addUser();
addTeacher();
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
可以看到发生异常时,被 try-catch 补获并抛出 exception 异常,但是因为 spring 事务,默认情况下只会回滚 RuntimeException(运行时异常)和 Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。所以一般都要求 Transactional 指定 rollbackfor。
4、抛出的异常被吃了
@Service
public class ServiceA {
@Transactional(rollbackFor=Exception.class)
public void add(UserModel userModel) throws Exception {
try {
addUser();
addTeacher();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
上面异常被 try-catch 吃掉了,且没有往外抛异常,所以这个时候事务是无法回滚。
5、@Transactional 的 propagation 参数设置不对
该参数的作用是指定事务的传播特性,spring 目前支持 7 种传播特性:
-
REQUIRED
如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。 -
SUPPORTS
如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。 -
MANDATORY
如果当前上下文中存在事务,否则抛出异常。 -
REQUIRES_NEW
每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。 -
NOT_SUPPORTED
如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。 -
NEVER
如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。 -
NESTED
如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
比如以下代码,设置为 never,这样在有事务情况下,会抛出异常
@Transactional(propagation=Propagation.NEVER)
public void test2() {
addUser();
addTeacher();
}
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
二、事务提交方式
1、声明式事务-自动提交
@Transactional(rollbackFor=Exception.class)
public void save(final User user) {
queryUser();
queryTeacher();
addUser();
addTeacher();
}
使用注解 Transactional,将整个方法包裹在事务中,但是其实 query 查询的方法不需要事务,真正需要事务的只有 addUser 和 addTeacher,造成了大事务。
2、编程式事务-手动提交
public void save(final User user) {
queryUser();
queryTeacher();
transactionTemplate.execute((status) => {
addUser();
addTeacher();
return Boolean.TRUE;
})
在需要提交事务的地方,手动进行提交,缩小事务的范围。
所以建议使用编程式事务,更小粒度的控制事务的范围。
关注公众号:臻大虾,分享更多java后端技术干货