其实关于spring事务失效的场景,网络上文章介绍的不少,参差不齐。这里只分享下自己的见解,时长大概10分钟左右,先上个图介绍下。
1.访问权限问题
事务方法需要定义public,非public方法事务会失效。事务拦截器TransactionalInterceptor会在执行方法前进行拦截,通过动态代理方式如果是cglib就是intercept方法或者jdk的invoke方法间接调用AbstractFallbackTransactionAttributeSource类的getTransactionAttribute方法获取配置信息,附上源码图:
进一步的跟踪getTransactionAttribute方法,我们就能看到,spring对于非public修饰的方式,返回的事务对象是null,其中allowPublicMethodsOnly返回的是一个布尔false。
2.方法被final修饰
事务底层使用了aop,那么也就是说通过jdk或者是cglib生成代理类,在代理类中实现的事务的功能,如果说方法是final修饰的了,那么就会导致代理类中无法重写该方法,从而导致添加事务失败。同样的如果是static的修饰的话也是无法通过动态代理变成事务方法。
3.方法内部调用
简单来说就是一个方法内部调用另一个方法,但是另一个方式是有事务的,这样也会导致事务失效,因为这个调用的是this对象的方法,而不是另一个方法持有的对象,可以这里理解。
如果想要在方法内部调用另一个方法也有事务的话,就需要新建一个service对象持有。
@Service
public class TmTrimServicebackImpl{
public void getById(Long id) {
TmTrimServiceliImpl.getTrimById(id);
}
}
@Service
public class TmTrimServiceliImpl{
@Transactional(rollbackFor=Exception.class)
public void getTrimById(Long id) {
TmTrimVO tmTrimVO = new TmTrimVO();
}
}
这样,通过新建一个service方法,将事务添加到新建的service方法里就可以了。说到这里可能小伙伴觉得这样有点麻烦,那么是否有没有其他的方式不新建一个方法呢,答案是可以的,就是注入自己,利用了spring ioc内部的三级缓存的机制,这里注入自己就很好的保证了也不会出现循环依赖:
@Service
public class TmTrimServicebackImpl{
@Autowired
private TmTrimServicebackImpl tmTrimServicebackImpl;
public void getById(Long id) {
tmTrimServicebackImpl.getTrimById(id);
}
@Transactional(rollbackFor=Exception.class)
public void getTrimById(Long id) {
TmTrimVO tmTrimVO = new TmTrimVO();
}
}
其实到了这一步,还是发现有点不太雅观,并不是说上面代码有什么问题只是觉得,可以让上面代码更加好看一点,那么有没有呢,答案是有的,是什么呢?这就不得不佩服spring强大完善的支持,那就是AopContext.currentProxy(),这个就是创建代理类,在方法调·调用前后切入,这个代理类对象是保存在ThreadLocal中的,所以通过这个代理类对象调用事务方法就能生效了。
@Service
public class TmTrimServicebackImpl{
public void getById(Long id) {
((TmTrimServicebackImpl)AopContext.currentProxy()).getTrimById(id);
}
@Transactional(rollbackFor=Exception.class)
public void getTrimById(Long id) {
TmTrimVO tmTrimVO = new TmTrimVO();
}
}
这样看来,代码是不是就优雅多了,哈哈!!!
4.未被spring事务管理
这里需要明确一个前提,就是使用spring事务的前提,就是对象要被spring管理就需要创建bean实例,在开发中,我们都是通过@Controller,@Service,@Component,@Repository等注解自动的实现依赖注入实例化的功能,但假如说在相应的控制层,业务层,数据层忘记加相应的注解,那么也是会失效的。因为没有交给spring管理,例如:
5.多线程调用
回想起前几年配置事务管理器时,都会有这样的一段配置:
通过这一段配置也可以知道,其实spring事务就是通过数据库连接事务多线程连接会导致持有的connetion不是同一个,从网上找了一张图,通过这张图进一步理解:
接着附上伪代码结合上面的图进一步理解:
@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发生异常,add方法也是不可能发生回滚的.这里需要解释以下什么是同一个事务,也就是说只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
6.多线程调用
这个就没什么好讲的了,也就是innodb和myisam引擎的不同,5版本以前默认是myisam引擎,这个引擎是不支持事务的,5版本以后的innodb是支持事务的。
7.未开启事务
这个其实可能也是比较容易忽略的,因为我们印象里好像没怎么配置过怎么开启事务,也确实是这样哈,为什么?其实原因很简单,springboot项目通过DataSourceTransactionManagerAutoConfiguration这个类已经默默的为我们开启了事务。
这个类会加载spring.datasource这个配置文件从而启动事务,如果是非springboot项目就需要自己手动在xml文件中配置事务管理器。
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
类似这样的从而开启事务。
8.错误的传播特性
在使用@Transactional注解时,是可以指定propagation参数的,该参数是用来指定事务的传播特性,其中只有required,requires_new,nested这三种才会创建新事务:
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
像上面的Propagation.NEVER这种类型的传播特性不支持事务,如果有事务则会抛异常。
9.自己吞了异常
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
像这种手动try...catch了异常,又没有手动抛出,那么sring就会认为程序是异常的就不会回滚了。
10.手动抛了别的异常
Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
捕获了异常又抛出了exception异常,事务同样不会回滚,因为spring事务默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),不会回滚,网上找了一张图:
这里exception里除了分为运行时异常和非运行时异常(ioException)。
1) 让checked例外也回滚:
在整个方法前加上 @Transactional(rollbackFor=Exception.class)
2) 让unchecked例外不回滚:
@Transactional(notRollbackFor=RunTimeException.class)
3)不需要事务管理的(只查询的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
这里需要提及的一句是,如果是自定义了的异常,比如说我自定义了DALException异常,那么就应该是@Transactional(notRollbackFor=DALException.class),一旦抛出的异常不属于DALException异常,那么事务也是不会生效的。
11.嵌套事务回滚多了
其实这个就有点像是js里的冒泡事件,可能我只是需要底部,结果外层窗口事件也触发了,联想到事务这里,那么也是一样的,嵌套多个可能只是想回滚对应的事务,就不用把其他事务也回滚了,这个可以通过try...catch来处理,将需要处理回滚的事务放这里面就不会把外层的也会滚了。
本文转载自微信公众号「小王子古木屋」,可以通过以下二维码关注。转载本文请联系小王子古木屋公众号。