1. 数据库引擎不支持事务
以mysql为例,其MyISAM引擎是不支持事务操作的,InnoDB才支持事务的引擎。从mysql5.5开始默认引擎是InnoDB,之前默认的是MyISAM,所以若底层引擎不支持事务注解也无能为力。。
2. 数据源没有配置事务管理器
3. 没有被spring管理
我们会把事务注解加到service层,如果没有@Service注解,这个类就不会被加载成一个Bean,那这个类就不会被spring管理,事务自然就失效了。。
4. 方法不是public的
根据spring官网,@Transactional只能用于public的方法上,否则事务不会生效,如果非要用在非public方法上,可以开启AspectJ代理模式。AspectJ使用加载时织入的方式,支持所有的pointcut,因此可以支持内部方法的事务设置。
5.@Transactional 注解属性 propagation 设置错误
如注解配置的不支持事务,使用了Propagation。NOT_SUPPORTED:表示不易事务运行,当前若存在事务则挂起,详细可以参考博客:Spring中的事务(六)_曰业而安的博客-CSDN博客
6. 同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法a,a再调用本类的方法b(不论方法b是用public还是private修饰),但方法A没有声明注解事务,而b方法有。则外部调用方法a之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实还是由于使用Spring AOP
代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring
生成的代理对象来管理。
//@Transactional
@GetMapping("/test")
private void a(User user) throws Exception {
userMapper.insert(user);
b(user);
}
@Transactional
public void b(User user) throws Exception {
doSomething();
}
补充:
如果a方法同样加了 @Transactional注解,此时的事务是否能生效呢?
上面的代码只是放开a方法上的@Transactional注解,此时a方法的事务依然是无法生效的。我们看到在事务方法a中,直接调用事务方法b。从前面介绍的内容可知,b方法拥有事务是因为spring aop生成了代理对象,但是这种方法直接调用了this对象的方法,所以a方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
那么问题来了,这种场景如何解决呢?
6.1 新加一个service方法
这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新的Service方法上,把需要事务执行的代码移到新方法中。
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
userMapper.insert(user);
b(user);
}
}
另外看到还有两种方法可以解决此问题,但感觉不是很直观。
6.2 在该Service类中注入自己
如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
userMapper.insert(user);
b(user);
}
}
这种做法不会造成循环依赖问题,其实spring IOC内部的三级缓存保证了它,不会出现循环依赖问题。
6.3 通过AopContext类
在该Service类中使用AopContext.currentProxy()获取代理对象
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
userMapper.insert(user);
b(user);
}
}
7. 异常被吃了
@Service
public class EmployeeServiceImpl implements EmployeeService{
@Transactional
public void updateEmployee(Employee employee){
try{
update(employee);
}catch(Exception e){
log.error(e.getMessage(),e);
}
}
}
把异常吃了,然后又不抛出来,事务怎么回滚呢?
如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
默默的说句,即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。
因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。
8. 异常类型错误【@Transactional 注解属性 rollbackFor 设置错误】
上面的例子再抛出一个异常
@Service
public class EmployeeServiceImpl implements EmployeeService{
@Transactional
public void updateEmployee(Employee employee){
try{
}catch{
throw new Exception(更新失败!!!"")
}
}
}
这样事务也是不生效的,因为默认回滚的是RuntimeException,如果你想要触发其它的异常回滚,需要在注解上配置:
@Transactional(rollbackFor = Exception.class)
这个配置仅限于Throwable异常类及其子类。
9. 方法用final修饰
有时候,某个方法不想被子类重写,这时可以将方法定义成final的。普通方法这样定义没问题,但是如果将事务定义为final,这样会导致事务失效。
为什么呢?
spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法被final修饰了,那么在它的代理类中,就无法重写该方法,事务也就失效了。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
10. 多线程调用
在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
希望对看到的小伙伴有所帮助,欢迎各位留言。。。