美文网首页springboot
【Java 开发常见的坑】——@Transactional 事务

【Java 开发常见的坑】——@Transactional 事务

作者: 爱打乒乓的程序员 | 来源:发表于2020-10-26 17:47 被阅读0次

    Spring中通过@Transactional注解动态代理对目标方法的增强,可以很方便的回滚事务。但是,如果不熟悉使用@Transactional注解的话,却会有很多隐藏的坑不容易被发现,往往是在线上环境才出现问题,通过一番排查才找到问题所在,以下是本人实际工作中或是浏览其他相关博客模拟实现的场景,以此加深记忆和记录。

    1.@Transactional注解标记的方法是private

    2.@Transactional注解标记的方法不是Spring注入的bean调用

    3.@Transactional注解没有显示声明rollbackFor属性

    4.@Transactional注解标记的方法内,使用try...catch捕获异常

    5.@Transactional注解使用默认的传播机制

    打开@Transactional注解的内容

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Transactional {
        @AliasFor("transactionManager")
        String value() default "";
    
        @AliasFor("value")
        String transactionManager() default "";
    
        Propagation propagation() default Propagation.REQUIRED;
    
        Isolation isolation() default Isolation.DEFAULT;
    
        int timeout() default -1;
    
        boolean readOnly() default false;
    
        Class<? extends Throwable>[] rollbackFor() default {};
    
        String[] rollbackForClassName() default {};
    
        Class<? extends Throwable>[] noRollbackFor() default {};
    
        String[] noRollbackForClassName() default {};
    }
    
    @Transactional属性详解

    废话不多说,直接上案例!

    以下的案例都是模拟新增用户的流程,为了简便,使用Spring Data JPA操作数据库。

    User实体类

     @Entity
     public class User {
         @Id
         @GeneratedValue(strategy = GenerationType.AUTO)
         private Integer id;
         private String name;
        
        // 省略getter、setter
     }
    

    UserDao类

    @Repository
    public interface UserDao extends JpaRepository<User,Integer> {
    }
    

    Controller类

    @RestController
    public class TransactionController {
        @Autowired
        private UserService userService;
    
        @GetMapping("test")
        public void test(){
            userService.createUser();
        }
    }
    

    1.@Transactional注解标记的方法是private

    接下来看下Service实现类

    @Service
    public class UserService{
    
        @Autowired
        private UserDao userDao;
    
        /**
         * 创建用户
         */
        public void createUser() {
            insertUser();
        }
    
        @Transactional
        private void insertUser(){
            User user = new User();
            user.setName("MuggleLee");
            userDao.save(user);
            throw new RuntimeException("错误");
        }
    }
    

    访问:http://localhost:8080/test后,可以发现控制台报错,证明有抛出异常,那么事务是否有回滚呢?查看一下数据库的User表,却发现有新增用户信息,就证明事务并没有回滚,事务回滚失效了!

    这是为什么呢?这就需要知道@Transactional的原理,实际上就是Spring中的AOP,使用@Transactional注解,Spring就会通过动态代理的方式增强目标方法。所以private的方法是无法被代理,所以动态代理失效,无法回滚事务!

    既然知道原因,那是不是将private方法改为public就行啦?

    @Service
    public class UserService{
    
        @Autowired
        private UserDao userDao;
    
        /**
         * 创建用户
         */
        public void createUser() {
            insertUser();
        }
    
        @Transactional
        public void insertUser(){
            User user = new User();
            user.setName("MuggleLee");
            userDao.save(user);
            throw new RuntimeException("错误");
        }
    }
    

    再次访问http://localhost:8080/test,虽然控制台有输出报错信息,但还是没有回滚数据库的操作,这就纳闷了,不是使用@Transactional注解就可以了吗?

    这就引申到下一个"坑"了

    2.@Transactional注解标记的方法不是Spring注入的bean调用

    有点拗口,其实简单理解为@Transactional注解标记的方法应该是Bean的调用,而不是方法内调用。例子中@Transactional注解标记的方法是由Bean内部方法的调用,所以将@Transactional注解放到例子中的createUser方法就可以了。

    @Service
    public class UserService{
    
        @Autowired
        private UserDao userDao;
    
        /**
         * 创建用户
         */
        @Transactional
        public void createUser() {
            insertUser();
        }
    
        public void insertUser(){
            User user = new User();
            user.setName("MuggleLee");
            userDao.save(user);
            throw new RuntimeException("错误");
        }
    }
    

    访问http://localhost:8080/test,这次数据表就没有新增用户信息了,就证明事务回滚。

    小结:使用@Transactional注解的方法,访问级别应该是public,而且应该是被Bean调用的方法

    3.@Transactional注解没有显示声明rollbackFor属性

    那我再对Service改一下,抛出的异常由原来的RuntimeException改为Exception

    @Service
    public class UserService{
    
        @Autowired
        private UserDao userDao;
    
        /**
         * 创建用户
         */
        @Transactional
        public void createUser() throws Exception {
            insertUser();
        }
    
        public void insertUser() throws Exception {
            User user = new User();
            user.setName("MuggleLee");
            userDao.save(user);
            throw new Exception("错误");
        }
    }
    

    访问http://localhost:8080/test,再次发现由新增用户信息。My God!这又是什么坑呀?

    其实,这是由于不熟悉@Transactional注解的原因。

    这是因为Spring框架的事务管理默认地只在发生不受控异常(RuntimeException和Error)时才进行事务回滚。也就是说,当事务方法抛出受控异常(Exception中除了RuntimeException及其子类以外的)时不会进行事务回滚。

    rollbackFor属性的默认值是 RuntimeException ,但是如果抛出的异常是 Exception 类型,@Transactional注解无法捕获异常,所以也就无法回滚事务。阿里巴巴规范建议使用@Transactional注解的时候显式地声明rollbackFor属性的值

    // @Transactional注解 rollbackFor 属性默认值
    @Transactional(rollbackFor = RuntimeException.class)
    
    错误使用:
    @Transactional
    public void test(){}
    
    正确使用:
    @Transactional(rollbackFor = Exception.class)
    public void test(){}
    

    ps.强烈建议大家在Idea上安装阿里巴巴规范插件,插件扫描代码,发现有不规范的地方就回有提示,使咱们的代码更加规范、更加优雅!

    blog-插件提示.jpg

    将原本使用 @Transactional 改为 @Transactional(rollbackFor = Exception.class)后,重新启动访问http://localhost:8080/test后可以发现,用户信息没有新增,就证明事务回滚了!

    小结:使用 @Transactional 注解的时候,为了避免隐藏的bug,一定要显式声明rollbackFor属性的值!

    4.@Transactional注解标记的方法内,使用try...catch捕获异常

    接下来,模拟另外一个坑,这也是一个十分常见的事务失效问题

    改动使用 @Transactional 注解的方法,将原本throw异常改为try...catch捕获异常

        /**
         * 创建用户
         */
        @Transactional(rollbackFor = Exception.class)
        public void createUser(){
            try {
                insertUser();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    访问http://localhost:8080/test后可以发现,用户信息新增了,就证明事务并没有回滚!

    这是因为异常信息在被@Transactional捕获之前被try...catch...捕获了,相对于try...catch..."吃"掉了异常,@Transactional就无法捕获异常,所以就无法回滚事务!

    那我想通过使用try...catch...捕获异常并做出一些补偿机制,怎么办?其实也是可以的,加上一行:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

        /**
         * 创建用户
         */
        @Transactional(rollbackFor = Exception.class)
        public void createUser(){
            try {
                insertUser();
            } catch (Exception e) {
                e.printStackTrace();
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                // 可以自定义出异常后的操作
            }
        }
    

    小结:使用@Transactional注解的时候,要注意异常信息会不会被try...catch...捕获。

    5.@Transactional注解使用默认的传播机制

    @Transactional注解中,有个属性propagation,默认的传播级别为Propagation.REQUIRED

    propagation属性的值有以下几种选择

    • Propagation.REQUIRED(默认):如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务
    • Propagation.SUPPORTS:如果当前存在事务,则加入事务,没有则以非事务方式运行
    • Propagation.MANDATORY:当前存在事务,则加入事务,不存在事务则抛出异常
    • Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
    • Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
    • Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
    • Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

    但要根据实际的业务场景选择事务传播级别,不一定默认的传播级别适用!

    假设现在的业务场景是,先创建用户信息,然后根据用户信息创建学生信息(Student表),但如果由于某些原因,创建学生信息失败,但不能影响用户信息的创建。所以创建用户信息和学生信息应该在不同的事务内,这样才不会相互影响,这样的话,使用@Transactional默认的传播级别就实现不了,但我们可以改变propagation属性值,改为Propagation.REQUIRES_NEW

    Student实体类

    @Entity
    public class Student {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
        private String name;
        private String classroom;
        
        // 省略getter、setter
    }
    

    StudentDao类

    @Repository
    public interface StudentDao extends JpaRepository<Student,Integer> {
    }
    

    StudentService实现类

    @Service
    public class StudentService {
        @Autowired
        private StudentDao studentDao;
    
        /**
         * 创建学生基本信息
         */
        @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
        public void createStudentInfo() throws Exception {
            Student student = new Student();
            student.setName("MuggleLee");
            student.setClassroom("高一一班");
            studentDao.save(student);
            throw new Exception("错误");
        }
    }
    

    UserService实现类

    @Service
    public class UserService {
    
        @Autowired
        private UserDao userDao;
    
        @Autowired
        private StudentService studentService;
    
        /**
         * 创建用户
         */
        @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
        public void createUser() {
            insertUser();
            try {
                studentService.createStudentInfo();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private void insertUser(){
            User user = new User();
            user.setName("MuggleLee");
            userDao.save(user);
        }
    }
    

    重启后访问http://localhost:8080/test,可以发现用户信息可以正常新增,但学生信息却没有新增,就证明学生新增信息被事务回滚,但不影响用户信息新增。

    以上都是常见的事务失效的场景,希望能够诸位在开发的时候,多加注意!

    如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~

    参考资料:
    极客时间——专栏:Java业务开发常见错误100例

    相关文章

      网友评论

        本文标题:【Java 开发常见的坑】——@Transactional 事务

        本文链接:https://www.haomeiwen.com/subject/epwsrktx.html