Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記

Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記

點擊上方“java進階架構師”,選擇右上角“置頂公衆號”
20大進階架構專題每日送達
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記

引言有點長

前端的寶寶會用ajax,用異步編程到快樂的不行~ 我們java也有異步,用起來比他們還快樂~ 我們biaji一個注(gǒupí)解(gāoyào),也是快樂風男...
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記
且看下面的栗子:
註冊一個用戶,給他的賬戶初始化積分(也可以想象成註冊獎勵),再給用戶發個註冊通知短信,再發個郵件,(只是舉栗子,切莫牛角大法),這樣一個流程,短信和郵件我覺得完全可以拆分出來,沒必要拖在在主流程上來(再補充上事務[ACID:原子性,一致性,隔離性,持久性]就好了):
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記
今天就這點業務,我在暗想,這不是一個註解就搞掂的嘛~~~ 哈哈哈
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記





結果不是想象中的那麼完美~~ 來看我的異(dind)步(ding)歷險記
我的首發原創博客地址:你的@Async就真的異步嗎 ☞ 異步歷險奇遇記[1]
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記
https://juejin.im/post/5d47a80a6fb9a06ad3470f9a 裏面有gitHub項目地址,關注我,裏面實戰多多哦~


奇遇一 循環依賴異常

看code:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserService userService;

     @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 保存用戶成功:" + user);
        userService.senMsg(user);
        userService.senEmail(user);
        return insert;
    }

    @Async
    @Override
    public Boolean senMsg(User user) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("發送短信中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",手機號:" + user.getMobile() + "發送短信成功");
        return true;
    }

    @Async
    @Override
    public Boolean senEmail(User user) {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("發送郵件中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",郵箱:" + user.getEmail() + "發送郵件成功");
        return true;
    }

結果:啓動不起來,Spring循環依賴問題。 Spring不是解決了循環依賴問題嗎,它是支持循環依賴的呀?怎麼會呢?
不可否認,在這之前我也是這麼堅信的,倘若你目前也和我有一樣堅挺的想法,那就讓異常UnsatisfiedDependencyException,has been injected into other beans [userServiceImpl] in its raw version as part of a circular reference,,來鼓勵你,擁抱你, 就是這麼的不給面子,赤裸裸的circular reference。
談到Spring Bean的循環依賴,有的小夥伴可能比較陌生,畢竟開發過程中好像對循環依賴這個概念無感知。其實不然,你有這種錯覺,那是因爲你工作在Spring的襁褓中,從而讓你“高枕無憂”~ 其實我們的代碼中肯定被我們寫了循環依賴,比如像這樣:

@Service
public class AServiceImpl implements AService {
    @Autowired
    private BService bService;
    ...
}
@Service
public class BServiceImpl implements BService {
    @Autowired
    private AService aService;
    ...
}

通過實驗總結出,出現使用@Async導致循環依賴問題的必要條件:

已開啓@EnableAsync的支持
@Async註解所在的Bean被循環依賴了

奇遇二 異步失效異常

那麼既然不能循環依賴,我們就不循環依賴,我們這麼來:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    SendService sendService;

    @Override
    @Transactional()
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 保存用戶成功:" + user);
        this.senMsg(user);
        this.senEmail(user);
        return insert;
    }

    @Async
    @Override
    public Boolean senMsg(User user) {
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",手機號:" + user.getMobile() + "發送短信成功");
        return true;
    }

    @Async
    @Override
    public Boolean senEmail(User user) {
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",郵箱:" + user.getEmail() + "發送郵件成功");
        return true;
    }

結果我們測試了幾把,我打印一下結果:

2019-08-05 21:59:32.304  INFO 14360 --- [nio-8080-exec-3] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2019-08-05 21:59:32.346 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert   : ==>  Preparing: insert into t_user (username, sex, mobile,email) values (?, ?, ?,?) 
2019-08-05 21:59:32.454 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert   : ==> Parameters: 王麻子(String), 男(String), 18820158833(String), [email protected](String)
2019-08-05 21:59:32.463 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert   : <==    Updates: 1
User 保存用戶成功:User(id=101, username=王麻子, mobile=18820158833, [email protected], sex=男, password=123435, createTime=Mon Aug 05 12:20:51 CST 2019, updateTime=null)
發送短信中:.....
http-nio-8080-exec-3給用戶id:101,手機號:18820158833發送短信成功
發送郵件中:.....
http-nio-8080-exec-3給用戶id:101,郵箱:[email protected]發送郵件成功

這不白瞎了嗎?感知不到我的愛,白寫了,難受~~線程依然是http-nio-8080-exec-3,那麼爲什麼了呢? 下面會講的哦,先說結論:
通過實驗總結出,出現使用@Async導致異步失效的原因:

在本類中使用了異步是不支持異步的
調用者其實是this,是當前對象,不是真正的代理對象userService,spring無法截獲這個方法調用 所以不在不在本類中去調用,網上的解決方法有applicationContext.getBean(UserService.class)和AopContext.currentProxy()

奇遇三 事務失效異常

那麼,既然不能用當前對象,那我們用代理,AopContext.currentProxy(),然鵝,你發現,報錯了,對Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.就他:
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記

但是你去網上百度就會發現,都這麼搞


@EnableAspectJAutoProxy(exposeProxy = true)

我也這麼搞,但是又報錯了,細細的看報錯內容,才發現少了個jar包這東西要配合切面織入,要配合,懂嗎?,來上包

  <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>

再來看爲撒: 這是一個性感的話題,exposeProxy = true它的作用就是啓用切面的自動代理,說人話就是暴露當前代理對象到當前線程綁定, 看個報錯Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.就是AopContext搞得鬼.

public final class AopContext {
    private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
    private AopContext() {
    }

    // 該方法是public static方法,說明可以被任意類進行調用
    public static Object currentProxy() throws IllegalStateException {
        Object proxy = currentProxy.get();

        // 它拋出異常的原因是當前線程並沒有綁定對象
        // 而給線程綁定對象的方法在下面:特別有意思的是它的訪問權限是default級別,也就是說只能Spring內部去調用
        if (proxy == null) {
            throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
        }
        return proxy;
    }

    // 它最有意思的地方是它的訪問權限是default的,表示只能給Spring內部去調用
    // 調用它的類有CglibAopProxy和JdkDynamicAopProxy
    @Nullable
    static Object setCurrentProxy(@Nullable Object proxy) {
        Object old = currentProxy.get();
        if (proxy != null) {
            currentProxy.set(proxy);
        } else {
            currentProxy.remove();
        }
        return old;
    }

}

所以我們要做啓用代理設置,讓代理生效,來走起,主線程的方法使用來調用異步方法,來測試走起: no code said niao:


@Service
public class UserServiceImpl implements UserService {

@Autowired
UserMapper userMapper;

@Override@Transactional(propagation = Propagation.REQUIRED)
public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 保存用戶成功:" + user);
        UserService currentProxy = UserService.class.cast(AopContext.currentProxy());
        currentProxy.senMsg(user);
        currentProxy.senEmail(user);
        int i = 1 / 0;
        return insert;
}
@Async  @Override  @Transactional(propagation = Propagation.REQUIRES_NEW)
public void senMsg(User user) {
        user.setUsername(Thread.currentThread().getName()+"發短信測試事務...."+ new Random().nextInt());
        userMapper.insert(user);
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",手機號:" + user.getMobile() + "發送短信成功");
}
@Async @Override @Transactional(propagation = Propagation.REQUIRES_NEW)
public void senEmail(User user) {
        user.setUsername("發郵件測試事務...."+ new Random().nextInt());
         userMapper.insert(user);
         int i = 1 / 0;
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",郵箱:" + user.getEmail() + "發送郵件成功");
}
}    

測試結果:
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記
如我們所願,事務和異步!都生效了

1. 異步線程SimpleAsyncTaskExecutor-1和SimpleAsyncTaskExecutor-2分別發短信和郵件,主線程保存用戶
2. 實際結果,主線程保存的那個用戶失敗,名字叫'發送短信'的也保存失敗,只有叫'發郵件'的用戶插入成功
3. 那麼就做到了事務的線程隔離,事務的互不影響,完美
4. 親,你這麼寫了嗎?這麼寫不優美,雖然有用,但是寫的另闢蹊徑啊

奇遇四 異步嵌套異常

來,我們看個更騷氣的,異步中嵌套異步,來上code:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Override@Transactional(propagation = Propagation.REQUIRED)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 保存用戶成功:" + user);
        UserService currentProxy = UserService.class.cast(AopContext.currentProxy());
        currentProxy.send(user);
        return insert;
    }

    @Async  @Override  @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void send(User user) {
        //發短信
        user.setUsername(Thread.currentThread().getName()+"發短信測試事務...."+ new Random().nextInt());
        userMapper.insert(user);
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",手機號:" + user.getMobile() + "發送短信成功");
        //發郵件
        UserService currentProxy = UserService.class.cast(AopContext.currentProxy());
        currentProxy.senEmail(user);
    }

     @Async @Override @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void senEmail(User user) {
        user.setUsername("發郵件測試事務...."+ new Random().nextInt());
         userMapper.insert(user);
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",郵箱:" + user.getEmail() + "發送郵件成功");
    }
}

看我們猜下結果? 數據庫會新增幾個數據?3個?2個?1個?0個?納尼報錯?
哈哈``` 上結果:

Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記
答案:只有一條數據主線程保存成功,短信和郵件的插入都失敗了,最主要的是還報錯了,又是那個~~~Set 'exposeProxy' property on Advised to 'true'磨人的小妖精
通過實驗總結出,出現導致異步嵌套使用失敗的原因:

在本類中使用了異步嵌套異步是不支持的
調用者其實被代理過一次了,再嵌套會出現'二次代理',其實是達不到代理了效果,因爲已經異步了.即時你在嵌套中不使用代理去獲取,即使不保存,但是事務和異步效果都會跟隨當前的代理,即嵌套的效果是達不到再次異步的.
解決辦法應該有,但是我覺得我還沒找到.這個寫法是我們應該規避的,我們應該遵循規範,啓用新的服務類去完成我們的異步工作

下面我們舉個栗子:正確的寫法,優雅的寫法


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Autowired
    SendService sendService;

    @Override@Transactional(propagation = Propagation.REQUIRED)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 保存用戶成功:" + user);
        sendService.senMsg(user);
        sendService.senEmail(user);
        return insert;
    }
}

---------------無責任分割線--------------------

@Service
public class SendServiceImpl implements SendService {

    @Override
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Boolean senMsg(User user) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("發送短信中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int i = 1 / 0;
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",手機號:" + user.getMobile() + "發送短信成功");
        return true;
    }

    @Async
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Boolean senEmail(User user) {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("發送郵件中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",郵箱:" + user.getEmail() + "發送郵件成功");
        return true;
    }
}

結果肯定完美:br/>![](https://s4.51cto.com/images/blog/202011/21/e2bc13931139262f4f278fbad470b5d2.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
因此當你看到你同事就在本類寫個方法標註上@Async然後調用,請制止他吧,做的無用功~~~(關鍵自己還以爲有用,這是最可怕的深坑~)
那我補充點:@EnableAspectJAutoProxy(exposeProxy = true)的作用: 此註解它導入了AspectJAutoProxyRegistrar,最終設置此註解的兩個屬性的方法爲:

public abstract class AopConfigUtils {
    ...正在加(sheng)載(lue)代碼中 請稍後....
    public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
        if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
            BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
            definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
        }
    }
    public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
        if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
            BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
            definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
        }
    }
}

看到此註解標註的屬性值最終都被設置到了internalAutoProxyCreator身上,也就是它:自動代理創建器。 首先我們需要明晰的是:@Async的代理對象並不是由自動代理創建器來創建的,而是由AsyncAnnotationBeanPostProcessor一個單純的BeanPostProcessor實現的,很顯然當執行AopContext.currentProxy()這句代碼的時候報錯了。@EnableAsync給容器注入的是AsyncAnnotationBeanPostProcessor,它用於給@Async生成代理,但是它僅僅是個BeanPostProcessor並不屬於自動代理創建器,因此exposeProxy = true對它無效。 所以AopContext.setCurrentProxy(proxy);這個set方法肯定就不會執行,所以,因此,但凡只要業務方法中調用AopContext.currentProxy()方法就鐵定拋異常~~

奇遇五 基本類型異常

看嘛,發短信其實是一些網關調用,我想寫個看短信,郵件發送成功的標誌,是否調用成功的狀態,來走起

....省略...UserService
---------------無責任分割線--------------------

@Service
public class SendServiceImpl implements SendService {

    @Override
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean senMsg(User user) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("發送短信中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",手機號:" + user.getMobile() + "發送短信成功");
        return true;
    }

    @Async
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean senEmail(User user) {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("發送郵件中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給用戶id:" + user.getId() + ",郵箱:" + user.getEmail() + "發送郵件成功");
        return true;
    }
}

瞪大眼睛看,我的返回結果是boolean,屬於基本類型,雖然沒有用,但是報錯了:

org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: 
public boolean com.boot.lea.mybot.service.impl.SendServiceImpl.senMsg(com.boot.lea.mybot.entity.User)

導致我的數據庫一條數據都沒有,影響到主線程了,可見問題發生在主線程觸發異步線程的時候,那我們找原因: 是走代理觸發的:我先找這個類CglibAopProxy再順藤摸瓜

/**
     * Process a return value. Wraps a return of {@code this} if necessary to be the
     * {@code proxy} and also verifies that {@code null} is not returned as a primitive.
     */
    private static Object proce***eturnType(Object proxy, Object target, Method method, Object retVal) {
        // Massage return value if necessary
        if (retVal != null && retVal == target &&
                !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            // Special case: it returned "this". Note that we can't help
            // if the target sets a reference to itself in another returned object.
            retVal = proxy;
        }
        Class<?> returnType = method.getReturnType();
        if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        return retVal;
    }

在這retVal == null && returnType != Void.TYPE && returnType.isPrimitive(),因爲我們的這種異步其實是不支持友好的返回結果的,我們的結果應該是void,因爲這個異步線程被主線程觸發後其實被當做一個任務提交到Spring的異步的一個線程池中進行異步的處理任務了,線程之間的通信是不能之間返回的,其實用這種寫法我們就應該用void去異步執行,不要有返回值,而且我們的返回值是isPrimitive(),是基本類型,剛好達標....
那麼我大聲喊出,使用異步的時候儘量不要有返回值,實在要有你也不能用基本類型.

奇遇六 返回異步結果

有些人就是難受,就是想要返回結果,那麼也是可以滴:但是要藉助Furtrue小姐姐的get()來進行線程之間的阻塞通信,畢竟小姐姐⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄害羞.
醬紫寫,你就可以阻塞等到執行任務有結果的時候去獲取真正的結果了,這個寫法和我之前的文章JAVA併發異步編程 原來十個接口的活現在只需要一個接口就搞定![2]是一樣的道理了

import com.boot.lea.mybot.service.AsyncService;
import com.boot.lea.mybot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service
@Async("taskExecutor")
public class AsyncServiceImpl implements AsyncService {

    @Autowired
    private UserService userService;

    @Override
    public Future<Long> queryUserMsgCount(final Long userId) {
        System.out.println("當前線程:" + Thread.currentThread().getName() + "=-=====queryUserMsgCount");
        long countByUserId = userService.countMsgCountByUserId(userId);
        return new AsyncResult<>(countByUserId);
    }

    @Override
    public Future<Long> queryCollectCount(final Long userId) {
        System.out.println("當前線程:" + Thread.currentThread().getName() + "=-====queryCollectCount");
        long collectCount = userService.countCollectCountByUserId(userId);
        return new AsyncResult<>(collectCount);
    }

你需要知道的九大異步注意事項

儘量不要在本類中異步調用br/>儘量不要有返回值
不能使用本類的私有方法或者非接口化加註@Async,因爲代理不到失效
異步方法不能使用static修飾

br/>異步類沒有使用@Component註解(或其他註解)導致spring無法掃描到異步類
類中需要使用@Autowired或@Resource等註解自動注入,不能自己手動new對象
br/>如果使用SpringBoot框架必須在啓動類中增加@EnableAsync註解
在調用Async方法的方法上標註@Transactional是管理調用方法的事務的
在Async方法上標註@Transactional是管理異步方法的事務,事務因線程隔離

你需要懂的異步原理

@Async的異步:

當我們想要在SpringBoot中方便的使用@Async註解開啓異步操作的時候,只需要實現AsyncConfigurer接口(這樣就配置了默認線程池配置,當然該類需要在Spring環境中,因爲是默認的,所以只能有一個,沒有多個實現類排優先級的說法),實現對線程池的配置,並在啓動類上加上@EnableAsync註解,即可使得@Async註解生效。br/>我們甚至可以不顯式的實現AsyncConfigurer,我們可以在Spring環境中配置多個Executor類型的Bean,在使用@Async註解時,將註解的value指定爲你Executor類型的BeanName,就可以使用指定的線程池來作爲任務的載體,這樣就使用線程池也更加靈活。
參考資料

[1]
你的@Async就真的異步嗎 ☞ 異步歷險奇遇記:
https://juejin.im/post/5d47a80a6fb9a06ad3470f9a
[2]
JAVA併發異步編程 原來十個接口的活現在只需要一個接口就搞定!: https://juejin.im/post/5d3c46d2f265da1b9163dbce
———— e n d ————


微服務、高併發、JVM調優、面試專欄等20大進階架構師專題請關注公衆號【Java進階架構師】後在菜單欄查看。
Spring異步編程 | 你的@Async就真的異步嗎?異步歷險奇遇記
看到這裏,說明你喜歡本文
你的轉發,是對我最大的鼓勵!在看亦是支持↓


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章