springboot

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

2020-10-26  本文已影响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属性的值有以下几种选择

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

假设现在的业务场景是,先创建用户信息,然后根据用户信息创建学生信息(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例

上一篇下一篇

猜你喜欢

热点阅读