Java Spring 事务
在Spring中事务管理的方式有两种,编程式事务和声明式事务。先详细介绍一下两种事务的实现方式配置类。
编程式事务
@Configuration@EnableTransactionManagement@ComponentScan("com.javashitang")public class AppConfig {@Beanpublic DruidDataSource dataSource() {DruidDataSource ds = new DruidDataSource();ds.setDriverClassName("com.mysql.jdbc.Driver");ds.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true");ds.setUsername("test");ds.setPassword("test");ds.setInitialSize(5);return ds;}@Beanpublic DataSourceTransactionManager dataSourceTransactionManager() {return new DataSourceTransactionManager(dataSource());}@Beanpublic JdbcTemplate jdbcTemplate(DataSource dataSource) {return new JdbcTemplate(dataSource);}@Beanpublic TransactionTemplate transactionTemplate() {return new TransactionTemplate(dataSourceTransactionManager());}}public interface UserService {void addUser(String name, String location);default void doAdd(String name) {};}@Servicepublic class UserServiceV1Impl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate TransactionTemplate transactionTemplate;@Overridepublic void addUser(String name, String location) {transactionTemplate.execute(new TransactionCallbackWithoutResult() {@Overrideprotected void doInTransactionWithoutResult(TransactionStatus status) {try {String sql = "insert into user (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{name});throw new RuntimeException("保存用户信息失败");} catch (Exception e) {e.printStackTrace();status.setRollbackOnly();}}});}}
可以看到编程式事务的方式并不优雅,因为业务代码和事务代码耦合到一块,当发生异常的时候还得需要手动回滚事务(比使用JDBC方便多类,JDBC得先关闭自动自动提交,然后根据情况手动提交或者回滚事务)
如果优化事务方法的执行?如何做?
「其实完全可以用AOP来优化这种代码,设置好切点,当方法执行成功时提交事务,当方法发生异常时回滚事务,这就是声明式事务的实现原理」
使用AOP后,当调用事务方法时,会调用到生成的代理对象,代理对象中加入了事务提交和回滚的逻辑。
声明式事务
Spring aop动态代理的方式有如下几种方法
- JDK动态代理实现(基于接口)(JdkDynamicAopProxy)
- CGLIB动态代理实现(动态生成子类的方式)(CglibAopProxy)
- AspectJ适配实现
Spring aop默认只会使用JDK和CGLIB来生成代理对象
@Transactional可以用在哪里?
@Transactional可以用在类,方法,接口上
- 用在类上,该类的所有
public方法都具有事务 - 用在方法上,方法具有事务。当类和方法同时配置事务的时候,方法的属性会覆盖类的属性
用在接口上,一般不建议这样使用,因为只有基于接口的代理会生效,如果Spring AOP使用cglib来实现动态代理,会导致事务失效(因为注解不能被继承)
@Transactional失效的场景@Transactional注解应用到非public方法(除非特殊配置,例如使用AspectJ 静态织入实现 AOP)- 自调用,因为
@Transactional是基于动态代理实现的 - 异常在代码中被try catch了
- 异常类型不正确,默认只支持
RuntimeException和Error,不支持检查异常 - 事务传播配置不符合业务逻辑
参考Spring官方文档介绍,摘要、译文如下:@Transactional注解应用到非public方法When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
译文
使用代理时,您应该只将
@Transactional注释应用于具有公共可见性的方法。如果使用@Transactional注释对受保护的、私有的或包可见的方法进行注释,则不会引发错误,但带注释的方法不会显示配置的事务设置。如果需要注释非公共方法,请考虑使用AspectJ(见下文)。
简言之:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
目前,如果@Transactional注解作用在非public方法上,编译器也会给与明显的提示。
「为什么只有**public**方法上的**@Transactional**注解才会生效?」
首相JDK动态代理肯定只能是public,因为接口的权限修饰符只能是public。cglib代理的方式是可以代理protected方法的(private不行,子类访问不了父类的private方法)如果支持protected,可能会造成当切换代理的实现方式时表现不同,增大出现bug的可能性,所以统一一下。
「如果想让非**public**方法也生效,可以考虑使用AspectJ」
自调用,因为@Transactional是基于动态代理实现的
当自调用时,方法执行不会经过代理对象,所以会导致事务失效。例如通过如下方式调用addUser方法时,事务会失效
// 事务失效@Servicepublic class UserServiceV2Impl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic void addUser(String name, String location) {doAdd(name);}@Transactionalpublic void doAdd(String name) {String sql = "insert into user (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{name});throw new RuntimeException("保存用户失败");}}
可以通过如下方式解决
@Autowired注入自己,假如为self,然后通过self调用方法@Autowired ApplicationContext,从ApplicationContext通过getBean获取自己,然后再调用// 事务生效@Servicepublic class UserServiceV2Impl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate UserService userService;@Overridepublic void addUser(String name, String location) {userService.doAdd(name);}@Override@Transactionalpublic void doAdd(String name) {String sql = "insert into user (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{name});throw new RuntimeException("保存用户失败");}}
异常在代码中被try catch了
这个逻辑从源码理解比较清晰,只有当执行事务抛出异常才能进入
completeTransactionAfterThrowing方法,这个方法里面有回滚的逻辑,如果事务方法都没抛出异常就只会正常提交 ```java // org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. // 执行事务方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); }
<a name="N2gaX"></a>#### 非Spring容器管理的bean基于这种失效场景,有工作经验的基本上是不会存在这种错误的;`@Service` 注解注释,`StudentServiceImpl` 类则不会被Spring容器管理,因此即使方法被`@Transactional`注解修饰,事务也亦然不会生效。<br />简单举例如下:```java//@Servicepublic class StudentServiceImpl implements StudentService {@Autowiredprivate StudentMapper studentMapper;@Autowiredprivate ClassService classService;@Override@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public void insertClassByException(StudentDo studentDo) throws CustomException {studentMapper.insertStudent(studentDo);throw new CustomException();}}
注解修饰的方法被类内部方法调用
这种失效场景是日常开发中最常踩坑的地方;在类A里面有方法a 和方法b, 然后方法b上面用 @Transactional加了方法级别的事务,在方法a里面 调用了方法b, 方法b里面的事务不会生效。为什么会失效呢?:
其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
@Servicepublic class ClassServiceImpl implements ClassService {@Autowiredprivate ClassMapper classMapper;public void insertClass(ClassDo classDo) throws CustomException {insertClassByException(classDo);}@Override@Transactional(propagation = Propagation.REQUIRED)public void insertClassByException(ClassDo classDo) throws CustomException {classMapper.insertClass(classDo);throw new RuntimeException();}}//测试用例:@Testpublic void insertInnerExceptionTest() throws CustomException {classDo.setClassId(2);classDo.setClassName("java_2");classDo.setClassNo("java_2");classService.insertClass(classDo);}
测试结果:
java.lang.RuntimeExceptionat com.qxy.common.service.impl.ClassServiceImpl.insertClassByException(ClassServiceImpl.java:34)at com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:27)at com.qxy.common.service.impl.ClassServiceImpl$$FastClassBySpringCGLIB$$a1c03d8.invoke(<generated>)
虽然业务代码报错了,但是数据库中已经成功插入数据,事务并未生效;
解决方案
类内部使用其代理类调用事务方法:以上方法略作改动
public void insertClass(ClassDo classDo) throws CustomException {// insertClassByException(classDo);((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);}//测试用例:@Testpublic void insertInnerExceptionTest() throws CustomException {classDo.setClassId(3);classDo.setClassName("java_3");classDo.setClassNo("java_3");classService.insertClass(classDo);}
业务代码抛出异常,数据库未插入新数据,达到目的,成功解决一个事务失效问题;
数据库数据未发生改变;
注意:一定要注意启动类上要添加@EnableAspectJAutoProxy(exposeProxy = true)注解,否则启动报错:
java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)at com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:28)
异常类型不正确,默认只支持RuntimeException和Error,不支持检查异常
异常体系图如下。当抛出检查异常时,Spring事务不会回滚。如果抛出任何异常都回滚,可以配置rollbackFor为Exception
@Transactional(rollbackFor = Exception.class)
解决方案:
@Transactional注解修饰的方法,加上rollbackfor属性值,指定回滚异常类型:@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Override@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)public void insertClassByException(ClassDo classDo) throws Exception {classMapper.insertClass(classDo);throw new Exception();}
捕获异常后,却未抛出异常
在事务方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。
@Servicepublic class ClassServiceImpl implements ClassService {@Autowiredprivate ClassMapper classMapper;// @Overridepublic void insertClass(ClassDo classDo) {((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);}@Override@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)public void insertClassByException(ClassDo classDo) {classMapper.insertClass(classDo);try {int i = 1 / 0;} catch (Exception e) {e.printStackTrace();}}}// 测试用例:@Testpublic void insertInnerExceptionTest() {classDo.setClassId(4);classDo.setClassName("java_4");classDo.setClassNo("java_4");classService.insertClass(classDo);}
解决方案:捕获异常并抛出异常
@Override@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)public void insertClassByException(ClassDo classDo) {classMapper.insertClass(classDo);try {int i = 1 / 0;} catch (Exception e) {e.printStackTrace();throw new RuntimeException();}}
事务传播配置不符合业务逻辑
假如说有这样一个场景,用户注册,依次保存用户基本信息到user表中,用户住址信息到地址表中,当保存用户住址信息失败时,也要保证用户信息注册成功。
public interface LocationService {void addLocation(String location);}@Servicepublic class LocationServiceImpl implements LocationService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Override@Transactionalpublic void addLocation(String location) {String sql = "insert into location (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{location});throw new RuntimeException("保存地址异常");}}@Servicepublic class UserServiceV3Impl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate LocationService locationService;@Override@Transactionalpublic void addUser(String name, String location) {String sql = "insert into user (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{name});locationService.addLocation(location);}}
调用发现user表和location表都没有插入数据,并不符合期望,可能会说抛出异常了,事务当然回滚了。把调用locationService的部分加上try catch
@Servicepublic class UserServiceV3Impl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate LocationService locationService;@Override@Transactionalpublic void addUser(String name, String location) {String sql = "insert into user (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{name});try {locationService.addLocation(location);} catch (Exception e) {e.printStackTrace();}}}
调用发现user表和location表还是都没有插入数据。这是因为在LocationServiceImpl中事务已经被标记成回滚了,所以最终事务还会回滚。
要想最终解决就不得不提到Spring的事务传播行为了。Transactional的事务传播行为默认为Propagation.REQUIRED。「如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务」
此时把LocationServiceImpl中Transactional的事务传播行为改成Propagation.REQUIRES_NEW即可
「创建一个新事务,如果当前存在事务,则把当前事务挂起」
所以最终的解决代码如下
@Servicepublic class UserServiceV3Impl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate LocationService locationService;@Override@Transactionalpublic void addUser(String name, String location) {String sql = "insert into user (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{name});try {locationService.addLocation(location);} catch (Exception e) {e.printStackTrace();}}}@Servicepublic class LocationServiceImpl implements LocationService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void addLocation(String location) {String sql = "insert into location (`name`) values (?)";jdbcTemplate.update(sql, new Object[]{location});throw new RuntimeException("保存地址异常");}}
事务传播行为设置异常
此种事务传播行为不是特殊自定义设置,基本上不会使用Propagation.NOT_SUPPORTED,不支持事务
@Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = Exception.class)public void insertClassByException(ClassDo classDo) {classMapper.insertClass(classDo);try {int i = 1 / 0;} catch (Exception e) {e.printStackTrace();throw new RuntimeException();}}
数据库存储引擎不支持事务
以MySQL关系型数据为例,如果其存储引擎设置为 MyISAM,则事务失效,因为MyISMA 引擎是不支持事务操作的;
故若要事务生效,则需要设置存储引擎为InnoDB ;目前 MySQL 从5.5.5版本开始默认存储引擎是:InnoDB;
