# 事务 @Transactional 注解的失效请狂

当我们使用 @Transactional 声明式事务时,有一些情况会导致事务并没有重启,因此总结一下我已经遇到了的 @Transactional 不启用的情况

# 1. 同一类里,未标注 @Transactional 的方法调用了标注 @Transactional 的方法

对我来说,这个是最常遇到的情况,因为声明式事务是根据 Spring AOP 来实现的 (即环绕通知,在方法执行前声明一个事务,并在方法结束后关闭事务), 但是以下这种情况就不会触发事务

题外话 : AOP 一共有三种织入方式

织入方式说明
编译时织入在编译的时候,把切面代码融合进来,需要特殊的编译器,AspectJ 属于这一类
类加载时织入在类被加载进内存的时候织入,这需要特殊的类加载器,AspectJ 实现了此类
运行时织入在运行的时候,通过动态代理的方式织入,调用切面代码增强业务,Spring 就是使用的这种方式,会带来性能上的开销,但是不用特殊的编译器或者类加载器

1
2
3
4
5
6
7
8
9
10
public class temp {
public void createFirst() {
this.createSecond();
}

@Transactional
public void createSecond() {
//do somethings
}
}

当外部方法新建一个 temp 类并且调用 createFirst 方法时,并不会触发事务,因为事务功能需要通过代理对象来实现,而在 createFirst() 方法中,调用了 this.createSecond() , 这里的 this 指向的是 temp 对象而不是代理对象,所以不会经过加强处理,就不会有事务支持了


我们可以先验证以下上面的对错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//这是mybatis-plus中ServiceImpl的源码, 这个方法支持事务操作
@Transactional(
rollbackFor = {Exception.class}
)
public boolean saveOrUpdate(T entity) {
if (null == entity) {
return false;
} else {
TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass);
Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!", new Object[0]);
String keyProperty = tableInfo.getKeyProperty();
Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!", new Object[0]);
Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity);
}
}
//这是controller层的源码, 用来模拟转账操作, @Transactional注解已经被注释掉了, 所以这就是上面外部方法调用无注释方法, 无注释方法调用有注释方法的情况
@GetMapping("transfer")
//@Transactional(rollbackFor = Exception.class)
public R transfer(@RequestBody List<Employee> employees) {
Employee rollOut = employees.get(0);
Employee shiftTo = employees.get(1);
if (rollOut == null || shiftTo == null) {
log.info("参数错误");
return new R(false, null, "参数错误");
} else {
UpdateWrapper<Employee> rollOutWrapper = new UpdateWrapper<>();
UpdateWrapper<Employee> shiftToWrapper = new UpdateWrapper<>();
rollOutWrapper.set(rollOut.getSalary() >= 500 && rollOut.getEmployeeId() != null, "salary", rollOut.getSalary() - 500);
shiftToWrapper.set(shiftTo.getEmployeeId() != null, "salary", shiftTo.getSalary() + 500);
rollOutWrapper.eq("employee_id", rollOut.getEmployeeId());
shiftToWrapper.eq("employee_id", shiftTo.getEmployeeId());
employeeService.saveOrUpdate(rollOut, rollOutWrapper);
//制造错误, 观察是否回滚
int i = 1 / 0;
employeeService.saveOrUpdate(shiftTo, shiftToWrapper);
return new R(true, true);
}
}

运行结果 :

20220603023209

运行前数据

运行后数据 :

20220603023951

运行后数据

可以看到, 只有一组数据发生了变化, 也就是说并没有回滚, 事务并没有启动

那我们有什么办法能让这种调用生效吗

刚刚分析了一下,出问题的地方在于 this 的指向问题,通过 this 直接绕过了代理类,所以如果我们指定代理类,也就是说将 this 换成代理类应该就可以解决问题了

但是不能用上面那个例子了,mybatis-plus 的源码太深了不好进行改动,我自己写一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//这是controller层, 用于调用service层的方法
@RestController
public class TempController {
@Autowired
private TempService service;

@GetMapping("/temp")
// @Transactional(rollbackFor = Exception.class)
public void temp() {
service.createFirst();
}
}

//这是service层, 里面的createFirst()方法调用了createSecond()方法, 而只有createSecond()方法声明了事务
public class TempServiceImpl implements TempService {
@Autowired
private EmployeeDao employeeDao;
@Override
public void createFirst() {
this.createSecond();
}

@Override
@Transactional(rollbackFor = Exception.class)
public void createSecond() {
Employee employee1 = employeeDao.selectById(123);
Employee employee2 = employeeDao.selectById(124);
UpdateWrapper<Employee> employee1Wrapper = new UpdateWrapper<>();
UpdateWrapper<Employee> employee2Wrapper = new UpdateWrapper<>();
employee1Wrapper.set(employee1.getSalary() >= 500 && employee1.getEmployeeId() != null, "salary", employee1.getSalary() - 500);
employee2Wrapper.set(employee1.getEmployeeId() != null, "salary", employee1.getSalary() + 500);
employee1Wrapper.eq("employee_id", employee1.getEmployeeId());
employee2Wrapper.eq("employee_id", employee1.getEmployeeId());
employeeDao.update(employee1, employee1Wrapper);
int i = 1 / 0;
employeeDao.update(employee2, employee2Wrapper);
}
}

运行结果 :
20220603121806
可以在 mybatis 的日志里面看到,使用的是非事务 SqlSession, 并没有回滚

稍微改一下代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RestController
public class TempController {
@Autowired
private TempService service;

@GetMapping("/temp")
// @Transactional(rollbackFor = Exception.class)
public void temp() {
//将service作为参数传递给了createFirst, 使得能在方法里面直接调用这个service的createSecond()方法
service.createFirst(service);
}
}


@Service
public class TempServiceImpl implements TempService {
@Autowired
private EmployeeDao employeeDao;
@Override
public void createFirst(TempService service) {
//这里使用了service.createSecond() 而不是 this.createSecond(), 因为service是经过AOP包装过的, 而使用this就绕过了这个包装类, 直接调用原始的方法了
service.createSecond();
}

@Override
@Transactional(rollbackFor = Exception.class)
public void createSecond() {
Employee employee1 = employeeDao.selectById(123);
Employee employee2 = employeeDao.selectById(124);
UpdateWrapper<Employee> employee1Wrapper = new UpdateWrapper<>();
UpdateWrapper<Employee> employee2Wrapper = new UpdateWrapper<>();
employee1Wrapper.set(employee1.getSalary() >= 500 && employee1.getEmployeeId() != null, "salary", employee1.getSalary() - 500);
employee2Wrapper.set(employee1.getEmployeeId() != null, "salary", employee1.getSalary() + 500);
employee1Wrapper.eq("employee_id", employee1.getEmployeeId());
employee2Wrapper.eq("employee_id", employee1.getEmployeeId());
employeeDao.update(employee1, employee1Wrapper);
int i = 1 / 0;
employeeDao.update(employee2, employee2Wrapper);
}
}

如果不出意外的话,那么应该这种方法就是事务类型的 :

20220603122931
可以看到,验证正确


# 访问修饰符导致的 @Transactional 失效

@Transactional 是基于 AOP 来完成事务操作的,所以对于一些 AOP 的失效情况,对于事务控制来说也会失效

点击查看 : AOP 的实现原理

AOP 是无法代理私有方法的,而 @Transactional 也会检测所注释的方法,若不为 Public 则不会触发事务,而且不会有警告或者报错,比较容易忽视