1. 臥槽,2行代碼,讓接口性能提升10倍?。。?/h1>

        共 16314字,需瀏覽 33分鐘

         ·

        2020-07-28 15:06


        1、本文內(nèi)容

        詳解 @EnableAsync & @Async,主要分下面幾個點進行介紹。

        1. 作用
        2. 用法
        3. 獲取異步執(zhí)行結果
        4. 自定義異步執(zhí)行的線程池
        5. 自定義異常處理
        6. 線程隔離
        7. 源碼 & 原理

        2、作用

        spring容器中實現(xiàn)bean方法的異步調(diào)用。

        比如有個logService的bean,logservice中有個log方法用來記錄日志,當調(diào)用logService.log(msg)的時候,希望異步執(zhí)行,那么可以通過@EnableAsync & @Async來實現(xiàn)。

        3、用法

        2步

        1. 需要異步執(zhí)行的方法上面使用@Async注解標注,若bean中所有的方法都需要異步執(zhí)行,可以直接將@Async加載類上。
        2. @EnableAsync添加在spring配置類上,此時@Async注解才會起效。

        常見2種用法

        1. 無返回值的
        2. 可以獲取返回值的

        4、無返回值的

        用法

        方法返回值不是Future類型的,被執(zhí)行時,會立即返回,并且無法獲取方法返回值,如:

        @Async
        public?void?log(String?msg)?throws?InterruptedException?{
        ????System.out.println("開始記錄日志,"?+?System.currentTimeMillis());
        ????//模擬耗時2秒
        ????TimeUnit.SECONDS.sleep(2);
        ????System.out.println("日志記錄完畢,"?+?System.currentTimeMillis());
        }

        案例

        實現(xiàn)日志異步記錄的功能。

        LogService.log方法用來異步記錄日志,需要使用@Async標注

        package?com.javacode2018.async.demo1;

        import?org.springframework.scheduling.annotation.Async;
        import?org.springframework.stereotype.Component;

        import?java.util.concurrent.TimeUnit;

        @Component
        public?class?LogService?{
        ????@Async
        ????public?void?log(String?msg)?throws?InterruptedException?{
        ????????System.out.println(Thread.currentThread()?+?"開始記錄日志,"?+?System.currentTimeMillis());
        ????????//模擬耗時2秒
        ????????TimeUnit.SECONDS.sleep(2);
        ????????System.out.println(Thread.currentThread()?+?"日志記錄完畢,"?+?System.currentTimeMillis());
        ????}
        }

        來個spring配置類,需要加上@EnableAsync開啟bean方法的異步調(diào)用.

        package?com.javacode2018.async.demo1;

        import?org.springframework.context.annotation.ComponentScan;
        import?org.springframework.context.annotation.EnableAspectJAutoProxy;
        import?org.springframework.scheduling.annotation.EnableAsync;

        @ComponentScan
        @EnableAsync
        public?class?MainConfig1?{
        }

        測試代碼

        package?com.javacode2018.async;

        import?com.javacode2018.async.demo1.LogService;
        import?com.javacode2018.async.demo1.MainConfig1;
        import?org.junit.Test;
        import?org.springframework.context.annotation.AnnotationConfigApplicationContext;

        import?java.util.concurrent.TimeUnit;

        public?class?AsyncTest?{

        ????@Test
        ????public?void?test1()?throws?InterruptedException?{
        ????????AnnotationConfigApplicationContext?context?=?new?AnnotationConfigApplicationContext();
        ????????context.register(MainConfig1.class);
        ????????context.refresh();
        ????????LogService?logService?=?context.getBean(LogService.class);
        ????????System.out.println(Thread.currentThread()?+?"?logService.log?start,"?+?System.currentTimeMillis());
        ????????logService.log("異步執(zhí)行方法!");
        ????????System.out.println(Thread.currentThread()?+?"?logService.log?end,"?+?System.currentTimeMillis());

        ????????//休眠一下,防止@Test退出
        ????????TimeUnit.SECONDS.sleep(3);
        ????}

        }

        運行輸出

        Thread[main,5,main]?logService.log?start,1595223990417
        Thread[main,5,main]?logService.log?end,1595223990432
        Thread[SimpleAsyncTaskExecutor-1,5,main]開始記錄日志,1595223990443
        Thread[SimpleAsyncTaskExecutor-1,5,main]日志記錄完畢,1595223992443

        前2行輸出,可以看出logService.log立即就返回了,后面2行來自于log方法,相差2秒左右。

        前面2行在主線程中執(zhí)行,后面2行在異步線程中執(zhí)行。

        5、獲取異步返回值

        用法

        若需取異步執(zhí)行結果,方法返回值必須為Future類型,使用spring提供的靜態(tài)方法org.springframework.scheduling.annotation.AsyncResult#forValue創(chuàng)建返回值,如:

        public?Future?getGoodsInfo(long?goodsId)?throws?InterruptedException?{
        ????return?AsyncResult.forValue(String.format("商品%s基本信息!",?goodsId));
        }

        案例

        場景:電商中商品詳情頁通常會有很多信息:商品基本信息、商品描述信息、商品評論信息,通過3個方法來或者這幾個信息。

        這3個方法之間無關聯(lián),所以可以采用異步的方式并行獲取,提升效率。

        下面是商品服務,內(nèi)部3個方法都需要異步,所以直接在類上使用@Async標注了,每個方法內(nèi)部休眠500毫秒,模擬一下耗時操作。

        package?com.javacode2018.async.demo2;

        import?org.springframework.scheduling.annotation.Async;
        import?org.springframework.scheduling.annotation.AsyncResult;
        import?org.springframework.stereotype.Component;

        import?java.util.Arrays;
        import?java.util.List;
        import?java.util.concurrent.Future;
        import?java.util.concurrent.TimeUnit;

        @Async
        @Component
        public?class?GoodsService?{
        ????//模擬獲取商品基本信息,內(nèi)部耗時500毫秒
        ????public?Future?getGoodsInfo(long?goodsId)?throws?InterruptedException?{
        ????????TimeUnit.MILLISECONDS.sleep(500);
        ????????return?AsyncResult.forValue(String.format("商品%s基本信息!",?goodsId));
        ????}

        ????//模擬獲取商品描述信息,內(nèi)部耗時500毫秒
        ????public?Future?getGoodsDesc(long?goodsId)?throws?InterruptedException?{
        ????????TimeUnit.MILLISECONDS.sleep(500);
        ????????return?AsyncResult.forValue(String.format("商品%s描述信息!",?goodsId));
        ????}

        ????//模擬獲取商品評論信息列表,內(nèi)部耗時500毫秒
        ????public?Future>?getGoodsComments(long?goodsId)?throws?InterruptedException?{
        ????????TimeUnit.MILLISECONDS.sleep(500);
        ????????List?comments?=?Arrays.asList("評論1",?"評論2");
        ????????return?AsyncResult.forValue(comments);
        ????}
        }

        來個spring配置類,需要加上@EnableAsync開啟bean方法的異步調(diào)用.

        package?com.javacode2018.async.demo2;

        import?org.springframework.context.annotation.ComponentScan;
        import?org.springframework.scheduling.annotation.EnableAsync;

        @ComponentScan
        @EnableAsync
        public?class?MainConfig2?{
        }

        測試代碼

        @Test
        public?void?test2()?throws?InterruptedException,?ExecutionException?{
        ????AnnotationConfigApplicationContext?context?=?new?AnnotationConfigApplicationContext();
        ????context.register(MainConfig2.class);
        ????context.refresh();
        ????GoodsService?goodsService?=?context.getBean(GoodsService.class);

        ????long?starTime?=?System.currentTimeMillis();
        ????System.out.println("開始獲取商品的各種信息");

        ????long?goodsId?=?1L;
        ????Future?goodsInfoFuture?=?goodsService.getGoodsInfo(goodsId);
        ????Future?goodsDescFuture?=?goodsService.getGoodsDesc(goodsId);
        ????Future>?goodsCommentsFuture?=?goodsService.getGoodsComments(goodsId);

        ????System.out.println(goodsInfoFuture.get());
        ????System.out.println(goodsDescFuture.get());
        ????System.out.println(goodsCommentsFuture.get());

        ????System.out.println("商品信息獲取完畢,總耗時(ms):"?+?(System.currentTimeMillis()?-?starTime));

        ????//休眠一下,防止@Test退出
        ????TimeUnit.SECONDS.sleep(3);
        }

        運行輸出

        開始獲取商品的各種信息
        商品1基本信息!
        商品1描述信息!
        [評論1,?評論2]
        商品信息獲取完畢,總耗時(ms):525

        3個方法總計耗時500毫秒左右。

        如果不采用異步的方式,3個方法會同步執(zhí)行,耗時差不多1.5秒,來試試,將GoodsService上的@Async去掉,然后再次執(zhí)行測試案例,輸出

        開始獲取商品的各種信息
        商品1基本信息!
        商品1描述信息!
        [評論1,?評論2]
        商品信息獲取完畢,總耗時(ms):1503

        這個案例大家可以借鑒一下,按照這個思路可以去優(yōu)化一下你們的代碼,方法之間無關聯(lián)的可以采用異步的方式,并行去獲取,最終耗時為最長的那個方法,整體相對于同步的方式性能提升不少。

        6、自定義異步執(zhí)行的線程池

        默認情況下,@EnableAsync使用內(nèi)置的線程池來異步調(diào)用方法,不過我們也可以自定義異步執(zhí)行任務的線程池。

        有2種方式來自定義異步處理的線程池

        方式1

        在spring容器中定義一個線程池類型的bean,bean名稱必須是taskExecutor

        @Bean
        public?Executor?taskExecutor()?{
        ????ThreadPoolTaskExecutor?executor?=?new?ThreadPoolTaskExecutor();
        ????executor.setCorePoolSize(10);
        ????executor.setMaxPoolSize(100);
        ????executor.setThreadNamePrefix("my-thread-");
        ????return?executor;
        }

        方式2

        定義一個bean,實現(xiàn)AsyncConfigurer接口中的getAsyncExecutor方法,這個方法需要返回自定義的線程池,案例代碼:

        package?com.javacode2018.async.demo3;

        import?com.javacode2018.async.demo1.LogService;
        import?org.springframework.beans.factory.annotation.Qualifier;
        import?org.springframework.context.annotation.Bean;
        import?org.springframework.lang.Nullable;
        import?org.springframework.scheduling.annotation.AsyncConfigurer;
        import?org.springframework.scheduling.annotation.EnableAsync;
        import?org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

        import?java.util.concurrent.Executor;

        @EnableAsync
        public?class?MainConfig3?{

        ????@Bean
        ????public?LogService?logService()?{
        ????????return?new?LogService();
        ????}

        ????/**
        ?????*?定義一個AsyncConfigurer類型的bean,實現(xiàn)getAsyncExecutor方法,返回自定義的線程池
        ?????*
        ?????*?@param?executor
        ?????*?@return
        ?????*/

        ????@Bean
        ????public?AsyncConfigurer?asyncConfigurer(@Qualifier("logExecutors")?Executor?executor)?{
        ????????return?new?AsyncConfigurer()?{
        ????????????@Nullable
        ????????????@Override
        ????????????public?Executor?getAsyncExecutor()?{
        ????????????????return?executor;
        ????????????}
        ????????};
        ????}

        ????/**
        ?????*?定義一個線程池,用來異步處理日志方法調(diào)用
        ?????*
        ?????*?@return
        ?????*/

        ????@Bean
        ????public?Executor?logExecutors()?{
        ????????ThreadPoolTaskExecutor?executor?=?new?ThreadPoolTaskExecutor();
        ????????executor.setCorePoolSize(10);
        ????????executor.setMaxPoolSize(100);
        ????????//線程名稱前綴
        ????????executor.setThreadNamePrefix("log-thread-");?//@1
        ????????return?executor;
        ????}

        }

        @1自定義的線程池中線程名稱前綴為log-thread-,運行下面測試代碼

        @Test
        public?void?test3()?throws?InterruptedException?{
        ????AnnotationConfigApplicationContext?context?=?new?AnnotationConfigApplicationContext();
        ????context.register(MainConfig3.class);
        ????context.refresh();
        ????LogService?logService?=?context.getBean(LogService.class);
        ????System.out.println(Thread.currentThread()?+?"?logService.log?start,"?+?System.currentTimeMillis());
        ????logService.log("異步執(zhí)行方法!");
        ????System.out.println(Thread.currentThread()?+?"?logService.log?end,"?+?System.currentTimeMillis());

        ????//休眠一下,防止@Test退出
        ????TimeUnit.SECONDS.sleep(3);
        }

        輸出

        Thread[main,5,main]?logService.log?start,1595228732914
        Thread[main,5,main]?logService.log?end,1595228732921
        Thread[log-thread-1,5,main]開始記錄日志,1595228732930
        Thread[log-thread-1,5,main]日志記錄完畢,1595228734931

        最后2行日志中線程名稱是log-thread-,正是我們自定義線程池中的線程。

        7、自定義異常處理

        異步方法若發(fā)生了異常,我們?nèi)绾潍@取異常信息呢?此時可以通過自定義異常處理來解決。

        異常處理分2種情況

        1. 當返回值是Future的時候,方法內(nèi)部有異常的時候,異常會向外拋出,可以對Future.get采用try..catch來捕獲異常
        2. 當返回值不是Future的時候,可以自定義一個bean,實現(xiàn)AsyncConfigurer接口中的getAsyncUncaughtExceptionHandler方法,返回自定義的異常處理器

        情況1:返回值為Future類型

        用法

        通過try..catch來捕獲異常,如下

        try?{
        ????Future?future?=?logService.mockException();
        ????System.out.println(future.get());
        }?catch?(ExecutionException?e)?{
        ????System.out.println("捕獲?ExecutionException?異常");
        ????//通過e.getCause獲取實際的異常信息
        ????e.getCause().printStackTrace();
        }?catch?(InterruptedException?e)?{
        ????e.printStackTrace();
        }

        案例

        LogService中添加一個方法,返回值為Future,內(nèi)部拋出一個異常,如下:

        @Async
        public?Future?mockException()?{
        ????//模擬拋出一個異常
        ????throw?new?IllegalArgumentException("參數(shù)有誤!");
        }

        測試代碼如下

        @Test
        public?void?test5()?throws?InterruptedException?{
        ????AnnotationConfigApplicationContext?context?=?new?AnnotationConfigApplicationContext();
        ????context.register(MainConfig1.class);
        ????context.refresh();
        ????LogService?logService?=?context.getBean(LogService.class);
        ????try?{
        ????????Future?future?=?logService.mockException();
        ????????System.out.println(future.get());
        ????}?catch?(ExecutionException?e)?{
        ????????System.out.println("捕獲?ExecutionException?異常");
        ????????//通過e.getCause獲取實際的異常信息
        ????????e.getCause().printStackTrace();
        ????}?catch?(InterruptedException?e)?{
        ????????e.printStackTrace();
        ????}
        ????//休眠一下,防止@Test退出
        ????TimeUnit.SECONDS.sleep(3);
        }

        運行輸出

        java.lang.IllegalArgumentException:?參數(shù)有誤!
        捕獲?ExecutionException?異常
        ?at?com.javacode2018.async.demo1.LogService.mockException(LogService.java:23)
        ?at?com.javacode2018.async.demo1.LogService$$FastClassBySpringCGLIB$$32a28430.invoke()
        ?at?org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

        情況2:無返回值異常處理

        用法

        當返回值不是Future的時候,可以自定義一個bean,實現(xiàn)AsyncConfigurer接口中的getAsyncUncaughtExceptionHandler方法,返回自定義的異常處理器,當目標方法執(zhí)行過程中拋出異常的時候,此時會自動回調(diào)AsyncUncaughtExceptionHandler#handleUncaughtException這個方法,可以在這個方法中處理異常,如下:

        @Bean
        public?AsyncConfigurer?asyncConfigurer()?{
        ????return?new?AsyncConfigurer()?{
        ????????@Nullable
        ????????@Override
        ????????public?AsyncUncaughtExceptionHandler?getAsyncUncaughtExceptionHandler()?{
        ????????????return?new?AsyncUncaughtExceptionHandler()?{
        ????????????????@Override
        ????????????????public?void?handleUncaughtException(Throwable?ex,?Method?method,?Object...?params)?{
        ????????????????????//當目標方法執(zhí)行過程中拋出異常的時候,此時會自動回調(diào)這個方法,可以在這個方法中處理異常
        ????????????????}
        ????????????};
        ????????}
        ????};
        }

        案例

        LogService中添加一個方法,內(nèi)部拋出一個異常,如下:

        @Async
        public?void?mockNoReturnException()?{
        ????//模擬拋出一個異常
        ????throw?new?IllegalArgumentException("無返回值的異常!");
        }

        來個spring配置類,通過AsyncConfigurer來自定義異常處理器AsyncUncaughtExceptionHandler

        package?com.javacode2018.async.demo4;

        import?com.javacode2018.async.demo1.LogService;
        import?org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
        import?org.springframework.context.annotation.Bean;
        import?org.springframework.lang.Nullable;
        import?org.springframework.scheduling.annotation.AsyncConfigurer;
        import?org.springframework.scheduling.annotation.EnableAsync;

        import?java.lang.reflect.Method;
        import?java.util.Arrays;

        @EnableAsync
        public?class?MainConfig4?{

        ????@Bean
        ????public?LogService?logService()?{
        ????????return?new?LogService();
        ????}

        ????@Bean
        ????public?AsyncConfigurer?asyncConfigurer()?{
        ????????return?new?AsyncConfigurer()?{
        ????????????@Nullable
        ????????????@Override
        ????????????public?AsyncUncaughtExceptionHandler?getAsyncUncaughtExceptionHandler()?{
        ????????????????return?new?AsyncUncaughtExceptionHandler()?{
        ????????????????????@Override
        ????????????????????public?void?handleUncaughtException(Throwable?ex,?Method?method,?Object...?params)?{
        ????????????????????????String?msg?=?String.format("方法[%s],參數(shù)[%s],發(fā)送異常了,異常詳細信息:",?method,?Arrays.asList(params));
        ????????????????????????System.out.println(msg);
        ????????????????????????ex.printStackTrace();
        ????????????????????}
        ????????????????};
        ????????????}
        ????????};
        ????}

        }

        運行輸出

        方法[public?void?com.javacode2018.async.demo1.LogService.mockNoReturnException()],參數(shù)[[]],發(fā)送異常了,異常詳細信息:
        java.lang.IllegalArgumentException:?無返回值的異常!
        ?at?com.javacode2018.async.demo1.LogService.mockNoReturnException(LogService.java:29)
        ?at?com.javacode2018.async.demo1.LogService$$FastClassBySpringCGLIB$$32a28430.invoke()
        ?at?org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

        8、線程池隔離

        什么是線程池隔離?

        一個系統(tǒng)中可能有很多業(yè)務,比如充值服務、提現(xiàn)服務或者其他服務,這些服務中都有一些方法需要異步執(zhí)行,默認情況下他們會使用同一個線程池去執(zhí)行,如果有一個業(yè)務量比較大,占用了線程池中的大量線程,此時會導致其他業(yè)務的方法無法執(zhí)行,那么我們可以采用線程隔離的方式,對不同的業(yè)務使用不同的線程池,相互隔離,互不影響。

        @Async注解有個value參數(shù),用來指定線程池的bean名稱,方法運行的時候,就會采用指定的線程池來執(zhí)行目標方法。

        使用步驟

        1. 在spring容器中,自定義線程池相關的bean
        2. @Async("線程池bean名稱")

        案例

        模擬2個業(yè)務:異步充值、異步提現(xiàn);2個業(yè)務都采用獨立的線程池來異步執(zhí)行,互不影響。

        異步充值服務
        package?com.javacode2018.async.demo5;

        import?org.springframework.scheduling.annotation.Async;
        import?org.springframework.stereotype.Component;

        @Component
        public?class?RechargeService?{
        ????//模擬異步充值
        ????@Async(MainConfig5.RECHARGE_EXECUTORS_BEAN_NAME)
        ????public?void?recharge()?{
        ????????System.out.println(Thread.currentThread()?+?"模擬異步充值");
        ????}
        }
        異步提現(xiàn)服務
        package?com.javacode2018.async.demo5;

        import?org.springframework.scheduling.annotation.Async;
        import?org.springframework.stereotype.Component;

        @Component
        public?class?CashOutService?{
        ????//模擬異步提現(xiàn)
        ????@Async(MainConfig5.CASHOUT_EXECUTORS_BEAN_NAME)
        ????public?void?cashOut()?{
        ????????System.out.println(Thread.currentThread()?+?"模擬異步提現(xiàn)");
        ????}
        }
        spring配置類

        注意@0、@1、@2、@3、@4這幾個地方的代碼,采用線程池隔離的方式,注冊了2個線程池,分別用來處理上面的2個異步業(yè)務。

        package?com.javacode2018.async.demo5;

        import?org.springframework.context.annotation.Bean;
        import?org.springframework.context.annotation.ComponentScan;
        import?org.springframework.scheduling.annotation.EnableAsync;
        import?org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

        import?java.util.concurrent.Executor;

        @EnableAsync?//@0:啟用方法異步調(diào)用
        @ComponentScan
        public?class?MainConfig5?{

        ????//@1:值業(yè)務線程池bean名稱
        ????public?static?final?String?RECHARGE_EXECUTORS_BEAN_NAME?=?"rechargeExecutors";
        ????//@2:提現(xiàn)業(yè)務線程池bean名稱
        ????public?static?final?String?CASHOUT_EXECUTORS_BEAN_NAME?=?"cashOutExecutors";

        ????/**
        ?????*?@3:充值的線程池,線程名稱以recharge-thread-開頭
        ?????*?@return
        ?????*/

        ????@Bean(RECHARGE_EXECUTORS_BEAN_NAME)
        ????public?Executor?rechargeExecutors()?{
        ????????ThreadPoolTaskExecutor?executor?=?new?ThreadPoolTaskExecutor();
        ????????executor.setCorePoolSize(10);
        ????????executor.setMaxPoolSize(100);
        ????????//線程名稱前綴
        ????????executor.setThreadNamePrefix("recharge-thread-");
        ????????return?executor;
        ????}

        ????/**
        ?????*?@4:?充值的線程池,線程名稱以cashOut-thread-開頭
        ?????*
        ?????*?@return
        ?????*/

        ????@Bean(CASHOUT_EXECUTORS_BEAN_NAME)
        ????public?Executor?cashOutExecutors()?{
        ????????ThreadPoolTaskExecutor?executor?=?new?ThreadPoolTaskExecutor();
        ????????executor.setCorePoolSize(10);
        ????????executor.setMaxPoolSize(100);
        ????????//線程名稱前綴
        ????????executor.setThreadNamePrefix("cashOut-thread-");
        ????????return?executor;
        ????}
        }
        測試代碼
        @Test
        public?void?test7()?throws?InterruptedException?{
        ????AnnotationConfigApplicationContext?context?=?new?AnnotationConfigApplicationContext();
        ????context.register(MainConfig5.class);
        ????context.refresh();

        ????RechargeService?rechargeService?=?context.getBean(RechargeService.class);
        ????rechargeService.recharge();
        ????CashOutService?cashOutService?=?context.getBean(CashOutService.class);
        ????cashOutService.cashOut();

        ????//休眠一下,防止@Test退出
        ????TimeUnit.SECONDS.sleep(3);
        }
        運行輸出
        Thread[recharge-thread-1,5,main]模擬異步充值
        Thread[cashOut-thread-1,5,main]模擬異步提現(xiàn)

        輸出中可以看出2個業(yè)務使用的是不同的線程池執(zhí)行的。

        9、源碼 & 原理

        內(nèi)部使用aop實現(xiàn)的,@EnableAsync會引入一個bean后置處理器:AsyncAnnotationBeanPostProcessor,將其注冊到spring容器,這個bean后置處理器在所有bean創(chuàng)建過程中,判斷bean的類上是否有@Async注解或者類中是否有@Async標注的方法,如果有,會通過aop給這個bean生成代理對象,會在代理對象中添加一個切面:org.springframework.scheduling.annotation.AsyncAnnotationAdvisor,這個切面中會引入一個攔截器:AnnotationAsyncExecutionInterceptor,方法異步調(diào)用的關鍵代碼就是在這個攔截器的invoke方法中實現(xiàn)的,可以去看一下。

        10、總結

        ](img/@EnableAsync & @Async.png)

        11、案例源碼

        https://gitee.com/javacode2018/spring-series

        1.?人人都能看懂的 6 種限流實現(xiàn)方案!

        2.?一個空格引發(fā)的“慘案“

        3.?大型網(wǎng)站架構演化發(fā)展歷程

        4.?Java語言“坑爹”排行榜TOP 10

        5. 我是一個Java類(附帶精彩吐槽)

        6. 看完這篇Redis緩存三大問題,保你能和面試官互扯

        7. 程序員必知的 89 個操作系統(tǒng)核心概念

        8. 深入理解 MySQL:快速學會分析SQL執(zhí)行效率

        9. API 接口設計規(guī)范

        10. Spring Boot 面試,一個問題就干趴下了!



        掃碼二維碼關注我


        ·end·

        —如果本文有幫助,請分享到朋友圈吧—

        我們一起愉快的玩耍!



        你點的每個贊,我都認真當成了喜歡

        瀏覽 58
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報

        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 无码一区二区三区四区五区电影在线 | 亚洲v视频 | 亚洲逼逼 | wwwxx国产 | 91视频专区 |