Java Spring 事务
在Spring中事务管理的方式有两种,编程式事务和声明式事务。先详细介绍一下两种事务的实现方式配置类。

编程式事务

  1. @Configuration
  2. @EnableTransactionManagement
  3. @ComponentScan("com.javashitang")
  4. public class AppConfig {
  5. @Bean
  6. public DruidDataSource dataSource() {
  7. DruidDataSource ds = new DruidDataSource();
  8. ds.setDriverClassName("com.mysql.jdbc.Driver");
  9. ds.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true");
  10. ds.setUsername("test");
  11. ds.setPassword("test");
  12. ds.setInitialSize(5);
  13. return ds;
  14. }
  15. @Bean
  16. public DataSourceTransactionManager dataSourceTransactionManager() {
  17. return new DataSourceTransactionManager(dataSource());
  18. }
  19. @Bean
  20. public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  21. return new JdbcTemplate(dataSource);
  22. }
  23. @Bean
  24. public TransactionTemplate transactionTemplate() {
  25. return new TransactionTemplate(dataSourceTransactionManager());
  26. }
  27. }
  28. public interface UserService {
  29. void addUser(String name, String location);
  30. default void doAdd(String name) {};
  31. }
  32. @Service
  33. public class UserServiceV1Impl implements UserService {
  34. @Autowired
  35. private JdbcTemplate jdbcTemplate;
  36. @Autowired
  37. private TransactionTemplate transactionTemplate;
  38. @Override
  39. public void addUser(String name, String location) {
  40. transactionTemplate.execute(new TransactionCallbackWithoutResult() {
  41. @Override
  42. protected void doInTransactionWithoutResult(TransactionStatus status) {
  43. try {
  44. String sql = "insert into user (`name`) values (?)";
  45. jdbcTemplate.update(sql, new Object[]{name});
  46. throw new RuntimeException("保存用户信息失败");
  47. } catch (Exception e) {
  48. e.printStackTrace();
  49. status.setRollbackOnly();
  50. }
  51. }
  52. });
  53. }
  54. }

可以看到编程式事务的方式并不优雅,因为业务代码和事务代码耦合到一块,当发生异常的时候还得需要手动回滚事务(比使用JDBC方便多类,JDBC得先关闭自动自动提交,然后根据情况手动提交或者回滚事务)
如果优化事务方法的执行?如何做?
「其实完全可以用AOP来优化这种代码,设置好切点,当方法执行成功时提交事务,当方法发生异常时回滚事务,这就是声明式事务的实现原理」
使用AOP后,当调用事务方法时,会调用到生成的代理对象,代理对象中加入了事务提交和回滚的逻辑。

声明式事务

Spring aop动态代理的方式有如下几种方法

  1. JDK动态代理实现(基于接口)(JdkDynamicAopProxy)
  2. CGLIB动态代理实现(动态生成子类的方式)(CglibAopProxy)
  3. AspectJ适配实现

Spring aop默认只会使用JDK和CGLIB来生成代理对象

@Transactional可以用在哪里?

@Transactional可以用在类,方法,接口上

  1. 用在类上,该类的所有public方法都具有事务
  2. 用在方法上,方法具有事务。当类和方法同时配置事务的时候,方法的属性会覆盖类的属性
  3. 用在接口上,一般不建议这样使用,因为只有基于接口的代理会生效,如果Spring AOP使用cglib来实现动态代理,会导致事务失效(因为注解不能被继承)

    @Transactional失效的场景

  4. @Transactional注解应用到非public方法(除非特殊配置,例如使用AspectJ 静态织入实现 AOP)

  5. 自调用,因为@Transactional是基于动态代理实现的
  6. 异常在代码中被try catch了
  7. 异常类型不正确,默认只支持RuntimeException和Error,不支持检查异常
  8. 事务传播配置不符合业务逻辑

    @Transactional注解应用到非public方法

    参考Spring官方文档介绍,摘要、译文如下:

    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方法时,事务会失效

  1. // 事务失效
  2. @Service
  3. public class UserServiceV2Impl implements UserService {
  4. @Autowired
  5. private JdbcTemplate jdbcTemplate;
  6. @Override
  7. public void addUser(String name, String location) {
  8. doAdd(name);
  9. }
  10. @Transactional
  11. public void doAdd(String name) {
  12. String sql = "insert into user (`name`) values (?)";
  13. jdbcTemplate.update(sql, new Object[]{name});
  14. throw new RuntimeException("保存用户失败");
  15. }
  16. }

可以通过如下方式解决

  1. @Autowired注入自己,假如为self,然后通过self调用方法
  2. @Autowired ApplicationContext,从ApplicationContext通过getBean获取自己,然后再调用

    1. // 事务生效
    2. @Service
    3. public class UserServiceV2Impl implements UserService {
    4. @Autowired
    5. private JdbcTemplate jdbcTemplate;
    6. @Autowired
    7. private UserService userService;
    8. @Override
    9. public void addUser(String name, String location) {
    10. userService.doAdd(name);
    11. }
    12. @Override
    13. @Transactional
    14. public void doAdd(String name) {
    15. String sql = "insert into user (`name`) values (?)";
    16. jdbcTemplate.update(sql, new Object[]{name});
    17. throw new RuntimeException("保存用户失败");
    18. }
    19. }

    异常在代码中被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); }

  1. <a name="N2gaX"></a>
  2. #### 非Spring容器管理的bean
  3. 基于这种失效场景,有工作经验的基本上是不会存在这种错误的;`@Service` 注解注释,`StudentServiceImpl` 类则不会被Spring容器管理,因此即使方法被`@Transactional`注解修饰,事务也亦然不会生效。<br />简单举例如下:
  4. ```java
  5. //@Service
  6. public class StudentServiceImpl implements StudentService {
  7. @Autowired
  8. private StudentMapper studentMapper;
  9. @Autowired
  10. private ClassService classService;
  11. @Override
  12. @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
  13. public void insertClassByException(StudentDo studentDo) throws CustomException {
  14. studentMapper.insertStudent(studentDo);
  15. throw new CustomException();
  16. }
  17. }

注解修饰的方法被类内部方法调用

这种失效场景是日常开发中最常踩坑的地方;在类A里面有方法a 和方法b, 然后方法b上面用 @Transactional加了方法级别的事务,在方法a里面 调用了方法b, 方法b里面的事务不会生效。为什么会失效呢?:
其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。

  1. @Service
  2. public class ClassServiceImpl implements ClassService {
  3. @Autowired
  4. private ClassMapper classMapper;
  5. public void insertClass(ClassDo classDo) throws CustomException {
  6. insertClassByException(classDo);
  7. }
  8. @Override
  9. @Transactional(propagation = Propagation.REQUIRED)
  10. public void insertClassByException(ClassDo classDo) throws CustomException {
  11. classMapper.insertClass(classDo);
  12. throw new RuntimeException();
  13. }
  14. }
  15. //测试用例:
  16. @Test
  17. public void insertInnerExceptionTest() throws CustomException {
  18. classDo.setClassId(2);
  19. classDo.setClassName("java_2");
  20. classDo.setClassNo("java_2");
  21. classService.insertClass(classDo);
  22. }

测试结果:

  1. java.lang.RuntimeException
  2. at com.qxy.common.service.impl.ClassServiceImpl.insertClassByException(ClassServiceImpl.java:34)
  3. at com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:27)
  4. at com.qxy.common.service.impl.ClassServiceImpl$$FastClassBySpringCGLIB$$a1c03d8.invoke(<generated>)

虽然业务代码报错了,但是数据库中已经成功插入数据,事务并未生效;

解决方案

类内部使用其代理类调用事务方法:以上方法略作改动

  1. public void insertClass(ClassDo classDo) throws CustomException {
  2. // insertClassByException(classDo);
  3. ((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);
  4. }
  5. //测试用例:
  6. @Test
  7. public void insertInnerExceptionTest() throws CustomException {
  8. classDo.setClassId(3);
  9. classDo.setClassName("java_3");
  10. classDo.setClassNo("java_3");
  11. classService.insertClass(classDo);
  12. }

业务代码抛出异常,数据库未插入新数据,达到目的,成功解决一个事务失效问题;
数据库数据未发生改变;
注意:一定要注意启动类上要添加@EnableAspectJAutoProxy(exposeProxy = true)注解,否则启动报错:

  1. java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
  2. at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)
  3. at com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:28)

异常类型不正确,默认只支持RuntimeExceptionError,不支持检查异常

异常体系图如下。当抛出检查异常时,Spring事务不会回滚。如果抛出任何异常都回滚,可以配置rollbackForException

  1. @Transactional(rollbackFor = Exception.class)

Spring声明式事务会失效的场景总结 - 图1

解决方案:

@Transactional注解修饰的方法,加上rollbackfor属性值,指定回滚异常类型:@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)

  1. @Override
  2. @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
  3. public void insertClassByException(ClassDo classDo) throws Exception {
  4. classMapper.insertClass(classDo);
  5. throw new Exception();
  6. }

捕获异常后,却未抛出异常

在事务方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。

  1. @Service
  2. public class ClassServiceImpl implements ClassService {
  3. @Autowired
  4. private ClassMapper classMapper;
  5. // @Override
  6. public void insertClass(ClassDo classDo) {
  7. ((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);
  8. }
  9. @Override
  10. @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
  11. public void insertClassByException(ClassDo classDo) {
  12. classMapper.insertClass(classDo);
  13. try {
  14. int i = 1 / 0;
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. // 测试用例:
  21. @Test
  22. public void insertInnerExceptionTest() {
  23. classDo.setClassId(4);
  24. classDo.setClassName("java_4");
  25. classDo.setClassNo("java_4");
  26. classService.insertClass(classDo);
  27. }

解决方案:捕获异常并抛出异常
  1. @Override
  2. @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
  3. public void insertClassByException(ClassDo classDo) {
  4. classMapper.insertClass(classDo);
  5. try {
  6. int i = 1 / 0;
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. throw new RuntimeException();
  10. }
  11. }

事务传播配置不符合业务逻辑

假如说有这样一个场景,用户注册,依次保存用户基本信息到user表中,用户住址信息到地址表中,当保存用户住址信息失败时,也要保证用户信息注册成功。

  1. public interface LocationService {
  2. void addLocation(String location);
  3. }
  4. @Service
  5. public class LocationServiceImpl implements LocationService {
  6. @Autowired
  7. private JdbcTemplate jdbcTemplate;
  8. @Override
  9. @Transactional
  10. public void addLocation(String location) {
  11. String sql = "insert into location (`name`) values (?)";
  12. jdbcTemplate.update(sql, new Object[]{location});
  13. throw new RuntimeException("保存地址异常");
  14. }
  15. }
  16. @Service
  17. public class UserServiceV3Impl implements UserService {
  18. @Autowired
  19. private JdbcTemplate jdbcTemplate;
  20. @Autowired
  21. private LocationService locationService;
  22. @Override
  23. @Transactional
  24. public void addUser(String name, String location) {
  25. String sql = "insert into user (`name`) values (?)";
  26. jdbcTemplate.update(sql, new Object[]{name});
  27. locationService.addLocation(location);
  28. }
  29. }

调用发现user表和location表都没有插入数据,并不符合期望,可能会说抛出异常了,事务当然回滚了。把调用locationService的部分加上try catch

  1. @Service
  2. public class UserServiceV3Impl implements UserService {
  3. @Autowired
  4. private JdbcTemplate jdbcTemplate;
  5. @Autowired
  6. private LocationService locationService;
  7. @Override
  8. @Transactional
  9. public void addUser(String name, String location) {
  10. String sql = "insert into user (`name`) values (?)";
  11. jdbcTemplate.update(sql, new Object[]{name});
  12. try {
  13. locationService.addLocation(location);
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }

调用发现user表和location表还是都没有插入数据。这是因为在LocationServiceImpl中事务已经被标记成回滚了,所以最终事务还会回滚。
要想最终解决就不得不提到Spring的事务传播行为了。
Transactional的事务传播行为默认为Propagation.REQUIRED「如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务」
此时把LocationServiceImpl中Transactional的事务传播行为改成Propagation.REQUIRES_NEW即可
「创建一个新事务,如果当前存在事务,则把当前事务挂起」
所以最终的解决代码如下

  1. @Service
  2. public class UserServiceV3Impl implements UserService {
  3. @Autowired
  4. private JdbcTemplate jdbcTemplate;
  5. @Autowired
  6. private LocationService locationService;
  7. @Override
  8. @Transactional
  9. public void addUser(String name, String location) {
  10. String sql = "insert into user (`name`) values (?)";
  11. jdbcTemplate.update(sql, new Object[]{name});
  12. try {
  13. locationService.addLocation(location);
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. @Service
  20. public class LocationServiceImpl implements LocationService {
  21. @Autowired
  22. private JdbcTemplate jdbcTemplate;
  23. @Override
  24. @Transactional(propagation = Propagation.REQUIRES_NEW)
  25. public void addLocation(String location) {
  26. String sql = "insert into location (`name`) values (?)";
  27. jdbcTemplate.update(sql, new Object[]{location});
  28. throw new RuntimeException("保存地址异常");
  29. }
  30. }

事务传播行为设置异常

此种事务传播行为不是特殊自定义设置,基本上不会使用Propagation.NOT_SUPPORTED,不支持事务

  1. @Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = Exception.class)
  2. public void insertClassByException(ClassDo classDo) {
  3. classMapper.insertClass(classDo);
  4. try {
  5. int i = 1 / 0;
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. throw new RuntimeException();
  9. }
  10. }

数据库存储引擎不支持事务

以MySQL关系型数据为例,如果其存储引擎设置为 MyISAM,则事务失效,因为MyISMA 引擎是不支持事务操作的;
故若要事务生效,则需要设置存储引擎为InnoDB ;目前 MySQL 从5.5.5版本开始默认存储引擎是:InnoDB;