简介
本文介绍Spring什么时候事务会失效以及如何解决。
Spring通过AOP进行事务的控制,如果操作数据库报异常,则会进行回滚;如果没有报异常则会提交事务。但是,有时候Spring事务会失效,本文将介绍Spring的事务何时会失效,以及如何避免事务失效。
情景1:异常类型错误
声明式事务和注解事务回滚的原理:当被切面切中或者是加了注解的方法中抛出了unchecked exception异常(默认情况)时,Spring会进行事务回滚。unchecked exception异常也就是:RuntimeException及其子类。
不回滚的情况
- 把异常给try catch了,没有手动抛出RuntimeException异常
- 抛出的异常不属于运行时异常(如IO异常),因为Spring默认情况下是捕获到RuntimeException就回滚
会回滚的情况
- 用了try catch,在catch里面再抛出一个 RuntimeException异常。
- 将Spring默认的回滚时的异常修改为Exception
- 这样可以让非运行时异常也要能回滚
- 在catch后写回滚代码来实现回滚。
- TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
- 这样就可以在抛异常后也能return返回值;比较适合需要拿到返回值的场景(),
情况2示例:
@Transactional(rollbackFor = { Exception.class }) public boolean test() { doDbSomeThing(); //其他操作 return true; }
情况3示例
/** TransactionAspectSupport手动回滚事务:*/ public boolean test() { try { doDbSomeThing(); } catch (Exception e) { e.printStackTrace(); //加上之后抛了异常就能回滚(有这句代码就不需要再手动抛出运行时异常了) TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return false; } return true; }
情景2:自调用
简介
如果在一个类里边,调用同类里边的方法,会导致被调用的方法事务失效,有如下几种情景:
情景1:无事务调用事务,被调用的方法事务失效,此时抛出了异常也不会回滚。
情景2:在REQUIRED级别调用REQUIRES_NEW级别时,进入REQUIRES_NEW级别的方法时没有新创建事务。但若REQUIRES_NEW级别的方法里抛了异常,则REQUIRED级别与REQUIRES_NEW级别的操作都会回滚。
复现
情景1:无事务调用事务
package com.example.demo.user.controller; import com.example.demo.user.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/test") public class TestController { @Autowired UserService userService; @PostMapping("/test") public void test() { userService.insertAndUpdate(); } }
package com.example.demo.user.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.user.entity.User; public interface UserService extends IService<User> { void insertAndUpdate(); }
package com.example.demo.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.user.entity.User; import com.example.demo.user.mapper.UserMapper; import com.example.demo.user.service.UserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public void insertAndUpdate() { User user = new User(); user.setName("Tony"); this.save(user); updateUser(user); } @Transactional public void updateUser(User user) { user.setAge(20); this.updateById(user); int i = 1 / 0; } }
测试结果:(事务失效。我们想要的是:id和name有值正常,age不应该有值)
JDBC Connection [HikariProxyConnection@769992042 wrapping com.mysql.cj.jdbc.ConnectionImpl@3eac1b48] will not be managed by Spring ==> Preparing: INSERT INTO t_user ( name ) VALUES ( ? ) ==> Parameters: Tony(String) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@113376db] Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a445157] was not registered for synchronization because synchronization is not active JDBC Connection [HikariProxyConnection@80636421 wrapping com.mysql.cj.jdbc.ConnectionImpl@3eac1b48] will not be managed by Spring ==> Preparing: UPDATE t_user SET name=?, age=? WHERE id=? ==> Parameters: Tony(String), 20(Integer), 1(Long) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a445157] 2021-05-19 21:58:13.902 ERROR 5948 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause java.lang.ArithmeticException: / by zero at ... ......
情景2:事务调用事务
package com.example.demo.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.user.entity.User; import com.example.demo.user.mapper.UserMapper; import com.example.demo.user.service.UserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override @Transactional(propagation = Propagation.REQUIRED) public void insertAndUpdate() { User user = new User(); user.setName("Tony"); this.save(user); updateUser(user); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateUser(User user) { user.setAge(20); this.updateById(user); } }
访问:http://127.0.0.1:8080/test/test
结果: (进入REQUIRES_NEW级别的方法时没有新创建事务)
Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] JDBC Connection [HikariProxyConnection@367846290 wrapping com.mysql.cj.jdbc.ConnectionImpl@7f014cae] will be managed by Spring ==> Preparing: INSERT INTO t_user ( name ) VALUES ( ? ) ==> Parameters: Tony(String) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] from current transaction ==> Preparing: UPDATE t_user SET name=?, age=? WHERE id=? ==> Parameters: Tony(String), 20(Integer), 2(Long) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@41d0b10]
解决方案
简介
bean的方法自调用时不会走动态代理,就无法执行到事务的AOP。解决方案就是:从容器中获取此类的bean,然后使用这个bean来调用方法,这样就能走代理。有以下三种方法:
- @Autowired注入自己(推荐)
- 通过ApplicationContext 获得自己
- 通过AopContext获取当前代理对象
法1:@Autowired注入自己(推荐)
package com.example.demo.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.user.entity.User; import com.example.demo.user.mapper.UserMapper; import com.example.demo.user.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired UserServiceImpl userServiceImpl; @Override public void insertAndUpdate() { User user = new User(); user.setName("Tony"); this.save(user); userServiceImpl.updateUser(user); } @Transactional public void updateUser(User user) { user.setAge(20); this.updateById(user); int i = 1 / 0; } }
法2:ApplicationContext 获得自己
package com.example.demo.common; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class ApplicationContextHolder implements ApplicationContextAware { private static ApplicationContext context; public void setApplicationContext(ApplicationContext context) throws BeansException { ApplicationContextHolder.context = context; } public static ApplicationContext getContext() { return context; } }
package com.example.demo.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.common.ApplicationContextHolder; import com.example.demo.user.entity.User; import com.example.demo.user.mapper.UserMapper; import com.example.demo.user.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public void insertAndUpdate() { User user = new User(); user.setName("Tony"); this.save(user); UserServiceImpl userServiceImpl = ApplicationContextHolder.getContext() .getBean(UserServiceImpl.class); userServiceImpl.updateUser(user); } @Transactional public void updateUser(User user) { user.setAge(20); this.updateById(user); int i = 1 / 0; } }
法3:AopContext获取代理对象
1.开启AspectJ动态代理
启动类加上:@EnableAspectJAutoProxy(exposeProxy = true)
package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @MapperScan("com.example.demo.**.mapper") @EnableAspectJAutoProxy(exposeProxy = true) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
2.导入aspect包
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency>
3.使用AopContext获取当前代理对象
package com.example.demo.user.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.user.entity.User; import com.example.demo.user.mapper.UserMapper; import com.example.demo.user.service.UserService; import org.springframework.aop.framework.AopContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public void insertAndUpdate() { User user = new User(); user.setName("Tony"); this.save(user); UserServiceImpl userServiceImpl = (UserServiceImpl) AopContext.currentProxy(); userServiceImpl.updateUser(user); } @Transactional public void updateUser(User user) { user.setAge(20); this.updateById(user); int i = 1 / 0; } }
原理
Spring的AopProxy.java通过调用getProxy,获取代理,然后通过反射执行方法时传的是代理类。
目前Spring实现动态代理的方式有两种,一种是cglib,一种是jdk的,两个的实现方式不一样,但是事务失效原因是一样的。
cglib要实现代理,就要实现MethodInterceptor接口,例如DynamicAdvisedInterceptor.java,最后通过反射执行方法时传的是目标类,不是代理类,也就是说我们通过aop执行A方法的时候,我们的通过反射调用的实例换成了目标类,这个就不会触发Spring的aop了。所以B方法的事务不会生效。
其他情景
失效原因 | 说明 |
只读事务 | 非只读事务才能回滚的,只读事务是不会回滚的 |
方法的权限修饰 | @Transactional 注解只能应用到 public 的方法上。 如果你在 protected、private 或 package 的方法上使用 @Transactional 注解,它不会报错,但事务会失效。 |
数据库引擎 | 如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB |
忘记配置bean | spring忘记配置扫描包,bean不在spring容器管理下 |
切入点表达式书写错误 | 如果采用声明式事务,一定要确保切入点表达式书写正确 |
加载配置 | @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。 |
请先
!