Async和Transactional注解使用时的动态代理问题

  张一帆   2021年12月11日


代理是指由被委托人接受委托人的委托全权去办理一件事情,在办理这件事情的过程中委托人会限制被委托人的行事边界。

哈喽 大家好,这次我想写一些关于我在工作中遇到的有关java动态代理问题的排查与解决,希望能够帮助自己总结的同时对网络上的其他小伙伴有些许的帮助。首先我要列举几个@Async和@Transactional的例子。

@Transactional

这种情况属于“自调用”的情况。自调用的意思就是通过CGLIB方式动态代理的方法,调用了类内其他方法。这种“自调用”的情况会使注解无效。具体代码如下:

  @Service
public class Galaxy {

    @Autowired
    private UserRepository userRepository;

    public void alpha(){
        beta();//这就是“自调用”:在Galaxy类内注解的方法内调用了同类内的其他方法。
    }

    //对beta()方法开启了事务
    @Transactional
    public void beta(){
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain(); //断点
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
    }
}

  在beta()方法中打下断点,数据库中是没有事务的。说明注解没有生效。

  mysql> select * from information_schema.innodb_trx \G;
Empty set (0.00 sec)

上面代码运行结果:

  mysql> select * from user;
+----+-------------+-----------+--------+
| id | account     | name      | pwd    |
+----+-------------+-----------+--------+
|  3 | zhouhuajina | 周华健    | 123456 |
|  4 | Jay zhou    | 周杰伦    | 123456 |
+----+-------------+-----------+--------+

  通过断点时的堆栈信息,我们发现实际调用beta()的是 Galaxy$$EnhancerBySpringCGLIB$$76b76f24方法。

  beta:44, Galaxy (com.example.service.galaxy)
alpha:31, Galaxy (com.example.service.galaxy)
invoke:-1, Galaxy$$EnhancerBySpringCGLIB$$76b76f24 (com.example.service.galaxy)
......

  Galaxy\(EnhancerBySpringCGLIB\)76b76f24方法是通过cglib做动态代理时被jvm建立起来的一个虚拟的文件,这个文件是对Galxy类的增强类(GalxyEnhence),增强类继承了Galaxy类并对注解的类进行方法增强。方法增强指的是你使用的那个注解会把相关代码(注解开发人员早已经写好了的代码)用重写的方式将你的代码增强起来。所以,这个被jvm虚拟出来的文件大概应该长这样:

  public class Galaxy$$EnhancerBySpringCGLIB$$76b76f24 extend Galaxy{
  
  		//因为beta方法被注解了,所以通过重写的方式增强这个方法。其他没有被注解的方法,不重写
     @Override
     public void beta(){
      try {
            // 开启事务
            startTransaction();
            UserDomain UserDomain = new UserDomain();
            UserDomain.setId(4L);
            UserDomain.setName("周杰伦");
            UserDomain.setAccount("Jay zhou");
            UserDomain.setPwd("123456");
            userRepository.save(UserDomain);
            UserDomain = new UserDomain();
            UserDomain.setId(3L);
            UserDomain.setName("周华健");
            UserDomain.setAccount("zhouhuajina");
            UserDomain.setPwd("123456");
            userRepository.save(UserDomain);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }
    
}

但是,这段代码出现了一个问题:beta()的调用者是隐含的this。这就是“自调用”的意思,那实际的调用链是:Galaxy.alpha() => Galaxy.beta()。它没有走我们的增强类的beta()!所以就出现了刚才的结果。按照这种思路可以这样改:

解决方案1:利用@Transactional的传递性,在总方法上加@Transactional。

  @Service
public class Galaxy {

    @Autowired
    private UserRepository userRepository;

    //解决方案1:将注解加到alpha上。
    //借助@transactional的传递性,会把beta()也加到事务里来。
  	@Transactional
    public void alpha(){
        beta();
    }
  
    public void beta(){
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain(); //断点
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
    }
}

  在beta()方法中打下断点,数据库中出现了事务,说明起效了。

  mysql> select * from information_schema.innodb_trx \G;
*************************** 1. row ***************************
                    trx_id: 422025326292896
                 trx_state: RUNNING
               trx_started: 2021-12-12 21:54:14
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 0
       trx_mysql_thread_id: 6022
                 trx_query: NULL
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 0
          trx_lock_structs: 0
     trx_lock_memory_bytes: 1128
           trx_rows_locked: 0
         trx_rows_modified: 0
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 0
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
       trx_schedule_weight: NULL
1 row in set (0.00 sec)

调用链为:Galaxy\(EnhancerBySpringCGLIB\)76b76f24.alpha()=> Galaxy\(EnhancerBySpringCGLIB\)76b76f24.beta()

解决方案2:在被代理类的外部调用其方法。

  // Galaxy.java
@Service
public class Galaxy {

    @Autowired
    private UserService userService;

    public void alpha(){
        userService.beta();//调用者换成了外部类userService。
    }
}

//UserService.java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    //将注解放到这里。
    @Transactional
    public void beta(){
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain();  //断点
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
    }
}

调用链为:Galaxy.alpha => UserService.beta()。这其实不能算是一种解决方案,而是一种解决思路。

解决方案3:自注入

  @Service
public class Galaxy {

  	//注解自己进入spring容器里。
  	@Autowired
  	private Galaxy galaxy
      
    @Autowired
    private UserRepository userRepository;
  	
    public void alpha(){
        galaxy.beta();  //beta()的调用者换成了galaxy类,就可以让spring直接从容器里拿增强类。
    }
  
    @Transactional
    public void beta(){
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain(); //断点
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
    }
}

  调用链为:Galaxy.alpha()=>Galaxy\(EnhancerBySpringCGLIB\)76b76f24.beta()

@Async 和 @Transactional

看下面的列子:

  @Service
public class Galaxy {

    @Autowired
    private UserRepository userRepository;

    @Transactional(rollbackFor = Exception.class)
    public void alpha() throws Exception{
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain();
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        beta();
    }

    @Async(value = "beta")
    public void beta() throws InterruptedException {
        Thread.sleep(3000);
        System.out.println("这是beta线程"); //断点
    }

}

  这个例子的意图是:alpha方法首先入库数据,然后调用异步方法beta。预期的效果应该是,alpha()直接插入数据,立刻退出。不会等到beta()中的数据输出。但是,实际情况却是alpha()在等待beta()3秒后,输出beta线程文案后退出。这意味着beta()上的@Async没有起效。

我在beta()中打了个断点,让我们看看当时的堆栈是什么情况:

  beta:47, Galaxy (com.example.service.galaxy)
alpha:41, Galaxy (com.example.service.galaxy)
。。。。。
alpha:-1, Galaxy$$EnhancerBySpringCGLIB$$d543df13 (com.example.service.galaxy)
。。。。。

按照刚才的理论来讲,应该是jvm制造出来了一个增强类叫 Galaxy$$EnhancerBySpringCGLIB$$d543df13,这个类继承了Galaxy类并且重写了alpha()和beta()。调用链应该是: Galaxy\(EnhancerBySpringCGLIB\)d543df13.alpha() => Galaxy.beta()。因为调用的是Galaxy.beta()而不是 Galaxy\(EnhancerBySpringCGLIB\)d543df13.beta(),所以@Async没有生效。

  但是,按照上面解决方案2来重新构造代码发现@Async仍然没有生效,这就有些矛盾了。通过搜索找到了这篇issue。根据文中所述,是@Async和@Transactional的order先后的顺序导致了这个问题。因为spring开发者认为@Async的顺序应该是最优先的,所以即使@Async和@Transactional放在了一起,也会是先实现异步再进行事务。那接下来让我们试一试:

  //Galaxy.java
@Service
public class Galaxy {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

  //1.先异步线程
    @Async(value = "alpha")
    public void alpha() throws Exception{
        System.out.println("alpha thread start");
        userService.beta(); //2.进入事务中
        System.out.println("alpha thread end");
        userService.gamma(); //3.抛出异常,回滚
    }
}


//UserService.java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Transactional(rollbackFor = Exception.class)
    public void beta() throws Exception{
        System.out.println("这是beta线程");
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain();
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
    }

    public void gamma() throws Exception {
        throw new Exception();
    }

}

  最终结果,符合预期,数据库里的数据被回滚了。然后再试一次两个注解在一起的情况:

  //Galaxy.java
@Service
public class Galaxy {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

  //1.先异步线程
    @Async(value = "alpha")
    @Transactional(rollbackFor = Exception.class)
    public void alpha() throws Exception{
        System.out.println("alpha thread start");
        userService.beta(); //2.进入事务中
        System.out.println("alpha thread end");
        userService.gamma(); //3.抛出异常,回滚
    }
}


//UserService.java
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public void beta() throws Exception{
        System.out.println("这是beta线程");
        UserDomain UserDomain = new UserDomain();
        UserDomain.setId(4L);
        UserDomain.setName("周杰伦");
        UserDomain.setAccount("Jay zhou");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
        UserDomain = new UserDomain();
        UserDomain.setId(3L);
        UserDomain.setName("周华健");
        UserDomain.setAccount("zhouhuajina");
        UserDomain.setPwd("123456");
        userRepository.save(UserDomain);
    }

    public void gamma() throws Exception {
        throw new Exception();
    }

}

  最终,也符合预期。最后,至于注解的时序原理等我有时间研究一下再写出来。