隨著RxJava
蒸殿、Reactor
等異步框架的流行,異步編程受到了越來(lái)越多的關(guān)注,尤其是在IO密集型的業(yè)務(wù)場(chǎng)景中拼窥,相比傳統(tǒng)的同步開(kāi)發(fā)模式裆泳,異步編程的優(yōu)勢(shì)越來(lái)越明顯叹洲。
那到底什么是異步編程?異步化真正的好處又是什么工禾?如何選擇適合自己團(tuán)隊(duì)的異步技術(shù)运提?在實(shí)施異步框架落地的過(guò)程中有哪些需要注意的地方?
本文從以下幾個(gè)方面結(jié)合真實(shí)項(xiàng)目異步改造經(jīng)驗(yàn)對(duì)異步編程進(jìn)行分析闻葵,希望能給大家一些客觀認(rèn)識(shí):
- 使用RxJava異步改造后的效果
- 什么是異步編程民泵?異步實(shí)現(xiàn)原理
- 異步技術(shù)選型參考
- 異步化真正的好處是什么?
- 異步化落地的難點(diǎn)及解決方案
- 擴(kuò)展:異步其他解決方案-協(xié)程
使用RxJava異步改造后的效果
下圖是我們后端java項(xiàng)目使用RxJava改造成異步前后的RT(響應(yīng)時(shí)長(zhǎng))效果對(duì)比:
統(tǒng)計(jì)數(shù)據(jù)基于App端的gateway槽畔,以75線為準(zhǔn)栈妆,還有80、85厢钧、90鳞尔、99線,從圖中可以看出改成異步后接口整體的平均響應(yīng)時(shí)長(zhǎng)降低了 40% 左右早直。
(響應(yīng)時(shí)間是以發(fā)送請(qǐng)求到收到后端接口響應(yīng)數(shù)據(jù)的時(shí)長(zhǎng)寥假,上圖改造的這個(gè)后端java接口內(nèi)部流程比較復(fù)雜,因?yàn)楣径际俏⒎?wù)架構(gòu)莽鸿,該接口內(nèi)部又調(diào)用了6個(gè)其他服務(wù)的接口昧旨,最后把這些接口的數(shù)據(jù)匯總在一起返回給前端)
這張圖是同步接口和改造成異步接口前后的CPU負(fù)載情況對(duì)比
改造前cpu load : 35.46
改造后cpu load : 14.25
改成異步后CPU的負(fù)載情況也有明顯下降,但CPU使用率并無(wú)影響(一般情況下異步化后cpu的利用率會(huì)有所提高祥得,但要看具體的業(yè)務(wù)場(chǎng)景)
CPU LoadAverage:一段時(shí)間內(nèi)處于可運(yùn)行狀態(tài)和不可中斷狀態(tài)的進(jìn)程平均數(shù)量兔沃。(可運(yùn)行分為正在運(yùn)行進(jìn)程和正在等待CPU的進(jìn)程;不可中斷則是它正在做某些工作不能被中斷比如等待磁盤(pán)IO级及、網(wǎng)絡(luò)IO等)
而我們的服務(wù)業(yè)務(wù)場(chǎng)景大部分都是IO密集型業(yè)務(wù)乒疏,功能實(shí)現(xiàn)很多需要依賴底層接口,會(huì)進(jìn)行頻繁的IO操作饮焦。
下圖是2019年在全球架構(gòu)師峰會(huì)上阿里分享的異步化改造后的RT和QPS效果:
什么是異步編程怕吴?
響應(yīng)式編程 + NIO
1.異步和同步的區(qū)別:
我們先從I/O的角度看下同步模式下接口A調(diào)用接口B的交互流程:
下圖是傳統(tǒng)的同步模式下io線程的交互流程,可以看出io是阻塞的县踢,即bio的運(yùn)行模式
接口A發(fā)起調(diào)用接口B后转绷,這段時(shí)間什么事情也不能做,主線程阻塞一直等到接口B數(shù)據(jù)返回硼啤,然后才能進(jìn)行其他操作议经,可想而知如果接口A調(diào)用的接口不止B的話(A->B->C->D->E。。煞肾。)咧织,那么等待的時(shí)間也是遞增的,而且這期間CPU也要一直占用著籍救,白白浪費(fèi)資源习绢,也就是上圖看到的 cpu load 高的原因。
而且還有一個(gè)隱患就是如果調(diào)用的其他服務(wù)中的接口比如C超時(shí)蝙昙,或接口C掛掉了闪萄,那么對(duì)調(diào)用方服務(wù)A來(lái)說(shuō),剩余的接口比如D耸黑、E都會(huì)無(wú)限等待下去桃煎。。大刊。
其實(shí)大部分情況下我們收到數(shù)據(jù)后內(nèi)部的處理邏輯耗時(shí)都很短,這個(gè)可以通過(guò)埋點(diǎn)執(zhí)行時(shí)間統(tǒng)計(jì)三椿,大部分時(shí)間都浪費(fèi)在了IO等待上缺菌。
下面這個(gè)視頻演示了同步模式下我們線上環(huán)境真實(shí)的接口調(diào)用情況,即接口調(diào)用的線程執(zhí)行和變化情況搜锰,(使用的工具是JDK自帶的jvisual來(lái)監(jiān)控線程變化情況)
這里先交代下大致背景:服務(wù)端api接口A內(nèi)部一共調(diào)用了6個(gè)其他服務(wù)的接口伴郁,大致交互是這樣的:
A接口(B -> C -> D -> E -> F -> G)返回聚合數(shù)據(jù)
背景:使用Jemter測(cè)試工具壓測(cè)100個(gè)線程并發(fā)請(qǐng)求接口,以觀察線程的運(yùn)行情況(可以全屏觀看):
視頻地址:
同步模式下io線程運(yùn)行情況
(綠色表示線程是"運(yùn)行中"狀態(tài)蛋叼,平均執(zhí)行時(shí)間大約8秒)
http-nio-8080-exec*
開(kāi)頭的是tomcat線程池中的線程焊傅,即前端請(qǐng)求我們后端接口時(shí)要通過(guò)tomcat服務(wù)器接收和轉(zhuǎn)發(fā)的線程,因?yàn)槲覀兒蠖薬pi接口內(nèi)部又調(diào)用了其他服務(wù)的6個(gè)接口(B狈涮、C狐胎、D、E歌馍、F握巢、G),同步模式下需要等待上一個(gè)接口返回?cái)?shù)據(jù)才能繼續(xù)調(diào)用下一個(gè)接口松却,所以可以從視頻中看出暴浦,大部分的http線程耗時(shí)都在8秒以上(綠色線條代表線程是"運(yùn)行中"狀態(tài),平均執(zhí)行時(shí)間大約8秒晓锻,包括等待接口返回的時(shí)間和我們內(nèi)部邏輯處理的總時(shí)間歌焦,因?yàn)槭潜镜丨h(huán)境測(cè)試,受機(jī)器和網(wǎng)絡(luò)影響較大)
然后我們?cè)倏聪庐惒侥J降慕换チ鞒萄舛撸磏io方式:
大致流程就是接口A發(fā)起調(diào)用接口B的請(qǐng)求后就立即返回独撇,而不用阻塞等待接口B響應(yīng),這樣的好處是
http-nio-8080-exec*
線程可以馬上得到復(fù)用,接著處理下一個(gè)前端請(qǐng)求的任務(wù)券勺,如果接口B處理完返回?cái)?shù)據(jù)后绪钥,會(huì)有一個(gè)回調(diào)線程池處理真正的響應(yīng),即這種模式下我們的業(yè)務(wù)流程是http線程只處理請(qǐng)求关炼,回調(diào)線程處理接口響應(yīng)程腹。
這個(gè)視頻演示了異步模式下接口A的線程執(zhí)行情況,同樣也是使用Jemter測(cè)試工具壓測(cè)100個(gè)線程并發(fā)請(qǐng)求接口儒拂,以觀察線程的運(yùn)行情況(可以全屏觀看):
視頻地址:
異步模式下io線程運(yùn)行情況
(綠色表示線程是"運(yùn)行中"狀態(tài)寸潦,平均執(zhí)行時(shí)間在1.8秒左右)
模擬的條件和同步模式一樣,同樣是100個(gè)線程并發(fā)請(qǐng)求接口社痛,但這次http-nio-8080-exec*
開(kāi)頭的線程只處理請(qǐng)求任務(wù)见转,而不再等待全部的接口返回,所以http的線程運(yùn)行時(shí)間普遍都很短(大部分在1.8秒左右完成)蒜哀,AsfThread-executor-*
是我們系統(tǒng)封裝的回調(diào)線程池斩箫,處理底層接口的真正響應(yīng)數(shù)據(jù)。
演示視頻中的AsfThread-executor-*
的回調(diào)線程只創(chuàng)建了30多個(gè)撵儿,而請(qǐng)求的http線程有100個(gè)乘客,也就是說(shuō)這30多個(gè)回調(diào)線程處理了接口B的100次響應(yīng)(其實(shí)應(yīng)該是600次,因?yàn)榻涌贐內(nèi)部又調(diào)用了6個(gè)其他接口淀歇,這6次也都是在異步線程里處理響應(yīng)的)易核,因?yàn)槊總€(gè)接口返回的時(shí)間不一樣,加上網(wǎng)絡(luò)傳輸?shù)臅r(shí)間浪默,所以可以利用這個(gè)時(shí)間差充分復(fù)用線程即cpu資源牡直,視頻中回調(diào)線程AsfThread-executor-*
的綠色運(yùn)行狀態(tài)是多段的,表示復(fù)用了多次纳决,也就是少量回調(diào)線程處理了全部(600次)的響應(yīng)碰逸,這正是IO多路復(fù)用的機(jī)制。
nio模式下雖然http-nio-8080-exec*
線程和回調(diào)線程AsfThread-executor-*
的運(yùn)行時(shí)間都很短岳链,但是從http線程開(kāi)始到asf回調(diào)處理完返回給前端結(jié)果的時(shí)間和bio即同步模式下的時(shí)間差異不大(在相同的邏輯流程下)花竞,并不是nio模式下服務(wù)響應(yīng)的整體時(shí)間就會(huì)縮短,而是會(huì)提升CPU的利用率掸哑,因?yàn)镃PU不再會(huì)阻塞等待(不可中斷狀態(tài)減少)约急,這樣CPU就能有更多的資源來(lái)處理其他的請(qǐng)求任務(wù),相同單位時(shí)間內(nèi)能處理更多的任務(wù)苗分,所以nio模式帶來(lái)的好處是:
- 提升QPS(用更少的線程資源實(shí)現(xiàn)更高的并發(fā)能力)
- 降低CPU負(fù)荷,提高利用率
2.nio原理
結(jié)合上面的接口交互圖可知厌蔽,接口B通過(guò)網(wǎng)絡(luò)返回?cái)?shù)據(jù)給調(diào)用方(接口A)這一過(guò)程,對(duì)應(yīng)底層實(shí)現(xiàn)就是網(wǎng)卡接收到返回?cái)?shù)據(jù)后摔癣,通過(guò)自身的DMA(直接內(nèi)存訪問(wèn))將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)奴饮,這一步不需要CPU參與操作纬向,也就是把原先CPU等待的事情交給了底層網(wǎng)卡去處理,這樣CPU就可以專注于我們的應(yīng)用程序即接口內(nèi)部的邏輯運(yùn)算戴卜。
3.nio In Java
nio在java里的實(shí)現(xiàn)主要是上圖中的幾個(gè)核心組件:
channel
逾条、buffer
、selector
投剥,這些組件組合起來(lái)即實(shí)現(xiàn)了上面所講的多路復(fù)用機(jī)制师脂,如下圖所示:響應(yīng)式編程
1.什么是響應(yīng)式編程?它和傳統(tǒng)的編程方式有什么區(qū)別江锨?
響應(yīng)式可以簡(jiǎn)單的理解為收到某個(gè)事件或通知后采取的一系列動(dòng)作吃警,如上文中所說(shuō)的響應(yīng)操作系統(tǒng)的網(wǎng)絡(luò)數(shù)據(jù)通知,然后以回調(diào)的方式處理數(shù)據(jù)啄育。
傳統(tǒng)的命令式編程主要由:順序酌心、分支、循環(huán) 等控制流來(lái)完成不同的行為
響應(yīng)式編程的特點(diǎn)是:
- 以邏輯為中心轉(zhuǎn)換為以數(shù)據(jù)為中心
- 從命令式到聲明式的轉(zhuǎn)換
2.Java.Util.Concurrent.Future
在Java使用nio后無(wú)法立即拿到真實(shí)的數(shù)據(jù)挑豌,而且先得到一個(gè)"future
"安券,可以理解為郵戳或快遞單,為了獲悉真正的數(shù)據(jù)我們需要不停的通過(guò)快遞單號(hào)查詢快遞進(jìn)度氓英,所以 J.U.C 中的 Future 是Java對(duì)異步編程的第一個(gè)解決方案完疫,通常和線程池結(jié)合使用,偽代碼形式如下:
ExecutorService executor = Executors.newCachedThreadPool(); // 線程池
Future<String> future = executor.submit(() ->{
Thread.sleep(200); // 模擬接口調(diào)用债蓝,耗時(shí)200ms
return "hello world";
});
// 在輸出下面異步結(jié)果時(shí)主線程可以不阻塞的做其他事情
// TODO 其他業(yè)務(wù)邏輯
System.out.println("異步結(jié)果:"+future.get()); //主線程獲取異步結(jié)果
Future
的缺點(diǎn)很明顯:
- 無(wú)法方便得知任務(wù)何時(shí)完成
- 無(wú)法方便獲得任務(wù)結(jié)果
- 在主線程獲得任務(wù)結(jié)果會(huì)導(dǎo)致主線程阻塞
3.ListenableFuture
Google并發(fā)包下的listenableFuture
對(duì)Java原生的future做了擴(kuò)展,顧名思義就是使用監(jiān)聽(tīng)器模式實(shí)現(xiàn)的回調(diào)機(jī)制盛龄,所以叫可監(jiān)聽(tīng)的future饰迹。
Futures.addCallback(listenableFuture, new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
System.out.println("異步結(jié)果:" + result);
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
}, executor);
回調(diào)機(jī)制的最大問(wèn)題是:Callback Hell(回調(diào)地獄)
試想如果調(diào)用的接口多了,而且接口之間有依賴的話余舶,最終寫(xiě)出來(lái)的代碼可能就是下面這個(gè)樣子:
- 代碼的字面形式和其所表達(dá)的業(yè)務(wù)含義不匹配
- 業(yè)務(wù)的先后關(guān)系在代碼層面變成了包含和被包含的關(guān)系
- 大量使用 Callback 機(jī)制啊鸭,使應(yīng)該是先后的業(yè)務(wù)邏輯在代碼形式上表現(xiàn)為層層嵌套,這會(huì)導(dǎo)致代碼難以理解和維護(hù)
那么如何解決 Callback Hell 問(wèn)題呢?
響應(yīng)式編程
主要是以下兩種解決方式:
- 事件驅(qū)動(dòng)機(jī)制
- 鏈?zhǔn)秸{(diào)用(Lambda)
4.CompletableFuture
Java8里的CompletableFuture
和Java9的Flow Api
勉強(qiáng)算是上面問(wèn)題的解決方案:
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() ->
"hello"
);
// f2依賴f1的結(jié)果做轉(zhuǎn)換
CompletableFuture<String> f2 = f1.thenApplyAsync(t ->
t + " world"
);
System.out.println("異步結(jié)果:" + f2.get());
但CompletableFuture
處理簡(jiǎn)單的任務(wù)可以使用匿值,但并不是一個(gè)完整的反應(yīng)式編程解決方案赠制,在服務(wù)調(diào)用復(fù)雜的情況下,存在服務(wù)編排挟憔、上下文傳遞钟些、柔性限流(背壓)方面的不足
如果使用CompletableFuture
面對(duì)這些問(wèn)題可能需要自己額外造一些輪子,Java9的Flow
雖然是基于Reactive Streams 規(guī)范實(shí)現(xiàn)的绊谭,但沒(méi)有RxJava政恍、Project Reactor這些異步框架豐富和強(qiáng)大和完整的解決方案。
當(dāng)然如果接口邏輯比較簡(jiǎn)單达传,完全可以使用listenableFuture
或CompletableFuture
篙耗,關(guān)于他們的詳細(xì)用法可參考之前的一篇文章:Java異步編程指南
5.Reactive Streams
在網(wǎng)飛推出RxJava1.0并在Android端普及流行開(kāi)后迫筑,響應(yīng)式編程的規(guī)范也呼之欲出:
https://www.reactive-streams.org/
包括后來(lái)的RxJava2.0、Project Reactor都是基于Reactive Streams規(guī)范實(shí)現(xiàn)的宗弯。
關(guān)于他們和listenableFuture
脯燃、 CompletableFuture
的區(qū)別通過(guò)下面的例子大家應(yīng)該就會(huì)清楚。
比如下面的基于回調(diào)的代碼示例:獲取用戶的5個(gè)收藏列表功能
圖中標(biāo)注序號(hào)的步驟對(duì)應(yīng)如下:
- 根據(jù)uid調(diào)用用戶收藏列表接口
userService.getFavorites
- 成功的回調(diào)邏輯
- 如果用戶收藏列表為空
- 調(diào)用推薦服務(wù)
suggestionService.getSuggestions
- 推薦服務(wù)成功后的回調(diào)邏輯
- 取前5條推薦并展示(
Java8 Stream api
) - 推薦服務(wù)失敗的回調(diào),展示錯(cuò)誤信息
- 如果用戶收藏列表有數(shù)據(jù)返回
- 取前5條循環(huán)調(diào)用詳情接口
favoriteService.getDetails
成功回調(diào)則展示詳情,失敗回調(diào)則展示錯(cuò)誤信息
可以看出主要邏輯都是在回調(diào)函數(shù)(onSuccess()
蒙保、onError()
)中處理的辕棚,在可讀性和后期維護(hù)成本上比較大。
基于Reactive Streams規(guī)范實(shí)現(xiàn)的響應(yīng)式編程解決方案如下:
- 調(diào)用用戶收藏列表接口
- 壓平數(shù)據(jù)流調(diào)用詳情接口
- 如果收藏列表為空調(diào)用推薦接口
- 取前5條
- 切換成異步線程處理上述聲明接口返回結(jié)果)
- 成功則展示正常數(shù)據(jù),錯(cuò)誤展示錯(cuò)誤信息
可以看出因?yàn)檫@些異步框架提供了豐富的api追他,所以我們可以把主要精力放在數(shù)據(jù)的流轉(zhuǎn)上坟募,而不是原來(lái)的邏輯控制上。這也是異步編程帶來(lái)的思想上的轉(zhuǎn)變邑狸。
下圖是RxJava的operator api
:
所以說(shuō)異步最吸引人的地方在于資源的充分利用,不把資源浪費(fèi)在等待的時(shí)間上(nio)单雾,代價(jià)是增加了程序的復(fù)雜度赚哗,而Reactive Program封裝了這些復(fù)雜性,使其變得簡(jiǎn)單硅堆。
因此我們無(wú)論使用哪種異步框架屿储,盡量使用框架提供的api,而不是像上圖那種基于回調(diào)業(yè)務(wù)的代碼渐逃,把業(yè)務(wù)邏輯都寫(xiě)在onSuccess够掠、onError等回調(diào)方法里,這樣無(wú)法發(fā)揮異步框架的真正作用:
Codes Like Sync茄菊,Works Like Async
即以同步的方式編碼疯潭,達(dá)到異步的效果與性能,兼顧可維護(hù)性與可伸縮性。
異步框架技術(shù)選型
上面這張圖也是阿里在2019年的深圳全球架構(gòu)師峰會(huì)上分享的PPT截圖(文章末尾有鏈接)面殖,供大家參考竖哩,選型標(biāo)準(zhǔn)主要是基于穩(wěn)定性、普及性脊僚、成本這3點(diǎn)考慮
如果是我個(gè)人更愿意選擇Project Reactor作為首選異步框架相叁,(具體差異網(wǎng)上很多分析,大家可以自行百度谷歌)辽幌,還有一點(diǎn)是因?yàn)镹etflix的尿性增淹,推出的開(kāi)源產(chǎn)品漸漸都不維護(hù)了,而且Project Reactor提供了reactor-adapter
組件舶衬,可以方便的和RxJava的api轉(zhuǎn)換埠通。
其實(shí)還有Vert.x也算異步框架 (底層使用netty實(shí)現(xiàn)nio, 最新版已支持reactive stream規(guī)范)
異步化真正的好處
Scalability
伸縮性主要體現(xiàn)在以下兩個(gè)方面:
- elastic 彈性
- resilient 容錯(cuò)性
(異步化在平時(shí)不會(huì)明顯降低 RT、提高 QPS逛犹,文章開(kāi)頭的數(shù)據(jù)也是在大促這種流量高峰下的體現(xiàn)出的異步效果)
從架構(gòu)和應(yīng)用等更高緯度看待異步帶來(lái)的好處則會(huì)提升系統(tǒng)的兩大能力:彈性 和 容錯(cuò)性
前者反映了系統(tǒng)應(yīng)對(duì)壓力的表現(xiàn)端辱,后者反映了系統(tǒng)應(yīng)對(duì)故障的表現(xiàn)
1.容錯(cuò)性
像RxJava梁剔,Reactor這些異步框架處理回調(diào)數(shù)據(jù)時(shí)一般會(huì)切換線程上下文,其實(shí)就是使用不同的線程池來(lái)隔離不同的數(shù)據(jù)流處理邏輯舞蔽,下圖說(shuō)明了這一特性的好處:
即利用異步框架支持線程池切換的特性實(shí)現(xiàn)服務(wù)/接口隔離荣病,進(jìn)而提高系統(tǒng)的高可用。
2.彈性
back-pressure是一種重要的反饋機(jī)制渗柿,相比于傳統(tǒng)的熔斷限流等方式个盆,是一種更加柔性的自適應(yīng)限流。使得系統(tǒng)得以優(yōu)雅地響應(yīng)負(fù)載朵栖,而不是在負(fù)載下崩潰颊亮。
異步化落地的難點(diǎn)及解決方案
還是先看下淘寶總結(jié)的異步改造中難點(diǎn)問(wèn)題:
中間件全異步牽涉到到公司中臺(tái)化戰(zhàn)略或框架部門(mén)的支持,包括公司內(nèi)部常用的中間件比如MQ陨溅、redis终惑、dal等,超出了本文討論的范圍门扇,感興趣的可以看下文章末尾的參考資料雹有。
線程模型統(tǒng)一的背景在上一節(jié)異步化好處時(shí)有提到過(guò),其實(shí)主要還是對(duì)線程池的管理臼寄,做好服務(wù)隔離霸奕,線程池設(shè)置和注意事項(xiàng)可以參考之前的兩篇文章:Java踩坑記系列之線程池 、線程池ForkJoinPool簡(jiǎn)介
這里主要說(shuō)下上下文傳遞和阻塞檢測(cè)的問(wèn)題:
1.上下文傳遞
改造成異步服務(wù)后吉拳,不能再使用ThreadLocal
傳遞上下文context
质帅,因?yàn)楫惒娇蚣鼙热鏡xJava一般在收到通知后會(huì)先調(diào)用observeOn()
方法切換成另外一個(gè)線程處理回調(diào),比如我們?cè)谡?qǐng)求接口時(shí)在ThreadLocal
的context里設(shè)置了一個(gè)值留攒,在回調(diào)線程里從context里取不到這個(gè)值的临梗,因?yàn)榇藭r(shí)已經(jīng)不是同一個(gè)ThreadLocal
了,所以需要我們手動(dòng)在切換上下文的時(shí)候傳遞context從一個(gè)線程到另一個(gè)線程環(huán)境稼跳,偽代碼如下:
Context context = ThreadLocalUtils.get(); // 獲取當(dāng)前線程的上下文
single.observeOn(scheduler).doOnEvent((data, error) -> ThreadLocalUtils.set(context)); // 切換線程后在doOnEvent里重新給新的線程賦值context
在observeOn()
方法切換成另外一個(gè)線程后調(diào)用doOnEvent
方法將原來(lái)的context
賦給新的線程ThreadLocal
注意:這里的代碼只是提供一種解決思路,實(shí)際在使用前和使用后還要考慮清空ThreadLocal
吃沪,因?yàn)榫€程有可能會(huì)回收到線程池下次復(fù)用汤善,而不是立即清理,這樣就會(huì)污染上下文環(huán)境票彪。
可以將傳遞上下文的方法封裝成公共方法红淡,不需要每次都手動(dòng)切換。
2.阻塞檢測(cè)
阻塞檢測(cè)主要是要能及時(shí)發(fā)現(xiàn)我們某個(gè)異步任務(wù)長(zhǎng)時(shí)間阻塞的發(fā)生降铸,比如異步線程執(zhí)行時(shí)間過(guò)長(zhǎng)進(jìn)而影響整個(gè)接口的響應(yīng)在旱,原來(lái)同步場(chǎng)景下我們的日志都是串行記錄到ES或Cat上的,現(xiàn)在改成異步后推掸,每次處理接口數(shù)據(jù)的邏輯可能在不同的線程中完成桶蝎,這樣記錄的日志就需要我們主動(dòng)去合并(依據(jù)具體的業(yè)務(wù)場(chǎng)景而定)驻仅,如果日志無(wú)法關(guān)聯(lián)起來(lái),對(duì)我們排查問(wèn)題會(huì)增加很多難度登渣。所幸的是隨著異步的流行噪服,現(xiàn)在很多日志和監(jiān)控系統(tǒng)都已支持異步了。
Project Reactor 自己也有阻塞檢測(cè)功能胜茧,可以參考這篇文章:BlockHound
3.其他問(wèn)題
除了上面提到的兩個(gè)問(wèn)題外粘优,還有一些比如RxJava2.0之后不支持返回null,如果我們?cè)瓉?lái)的代碼或編程習(xí)慣所致返回結(jié)果有null的情況呻顽,可以考慮使用java8的Optional.ofNullable()
包裝一下雹顺,然后返回的RxJava類型是這樣的:Single<Optional>
,其他異步框架如果有類似的問(wèn)題同理廊遍。
異步其他解決方案:纖程/協(xié)程
- Quasar
- Kilim
- Kotlin
- Open JDK Loom
- AJDK wisp2
協(xié)程并不是什么新技術(shù)嬉愧,它在很多語(yǔ)言中都有實(shí)現(xiàn),比如 Python
昧碉、Lua
英染、Go
都支持協(xié)程。
協(xié)程與線程不同之處在于被饿,線程由內(nèi)核調(diào)度四康,而協(xié)程的調(diào)度是進(jìn)程自身完成的。這樣就可以不受操作系統(tǒng)對(duì)線程數(shù)量的限制狭握,一個(gè)線程內(nèi)部可以創(chuàng)建成千上萬(wàn)個(gè)協(xié)程闪金。因?yàn)樯衔闹v到的異步技術(shù)都是基于線程的操作和封裝,Java中的線程概念對(duì)應(yīng)的就是操作系統(tǒng)的線程论颅。
1.Quasar哎垦、Kilim
開(kāi)源的Java輕量級(jí)線程(協(xié)程)框架,通過(guò)利用Java instrument
技術(shù)對(duì)字節(jié)碼進(jìn)行修改恃疯,使方法掛起前后可以保存和恢復(fù)JVM棧幀漏设,方法內(nèi)部已執(zhí)行到的字節(jié)碼位置也通過(guò)增加狀態(tài)機(jī)的方式記錄,在下次恢復(fù)執(zhí)行可直接跳轉(zhuǎn)至最新位置今妄。
2.Kotlin
Kotlin Coroutine 協(xié)程庫(kù)郑口,因?yàn)?Kotlin 的運(yùn)行依賴于 JVM,不能對(duì) JVM 進(jìn)行修改盾鳞,因此Kotlin不能在底層支持協(xié)程犬性。同時(shí)Kotlin 是一門(mén)編程語(yǔ)言,需要在語(yǔ)言層面支持協(xié)程腾仅,所以Kotlin 對(duì)協(xié)程支持最核心的部分是在編譯器中完成乒裆,這一點(diǎn)其實(shí)和Quasar、Kilim實(shí)現(xiàn)原理類似推励,都是在編譯期通過(guò)修改字節(jié)碼的方式實(shí)現(xiàn)協(xié)程
3.Project Loom
Project Loom 發(fā)起的原因是因?yàn)殚L(zhǎng)期以來(lái)Java 的線程是與操作系統(tǒng)的線程一一對(duì)應(yīng)的鹤耍,這限制了 Java 平臺(tái)并發(fā)能力提升肉迫,Project Loom 是從 JVM 層面對(duì)多線程技術(shù)進(jìn)行徹底的改變。
OpenJDK 在2018年創(chuàng)建了 Loom 項(xiàng)目惰蜜,目標(biāo)是在JVM上實(shí)現(xiàn)輕量級(jí)的線程昂拂,并解除JVM線程與內(nèi)核線程的映射。其實(shí) Loom 項(xiàng)目的核心開(kāi)發(fā)人員正是從Quasar項(xiàng)目過(guò)來(lái)的抛猖,目的也很明確格侯,就是要將這項(xiàng)技術(shù)集成到底層JVM里,所以Quasar項(xiàng)目目前已經(jīng)不維護(hù)了财著。联四。。
4.AJDK Wisp2
Alibaba Dragonwell 是阿里巴巴的 Open JDK 發(fā)行版撑教,提供長(zhǎng)期支持朝墩。dragonwell8已開(kāi)源協(xié)程功能(之前的版本是不支持的),開(kāi)啟jvm命令:-XX:+UseWisp2
即支持協(xié)程伟姐。
總結(jié)
- Future 在異步方面支持有限
- Callback 在編排能力方面有 Callback Hell 的短板
- Project Loom 最新支持的Open JDK版本是16收苏,目前還在測(cè)試中
- AJDK wisp2 需要換掉整個(gè)JVM,需要考慮改動(dòng)成本和收益比
所以目前實(shí)現(xiàn)異步化比較成熟的方案是 Reactive Streams
以上是老K對(duì)異步編程的理解愤兵,如有問(wèn)題歡迎指正鹿霸。
另外開(kāi)發(fā)人員也不要將自己局限在某種特定技術(shù)上,對(duì)各種技術(shù)都保持開(kāi)放的態(tài)度是開(kāi)發(fā)人員技能不斷提高的前提秆乳。只會(huì)簡(jiǎn)單說(shuō)某某語(yǔ)言懦鼠、某某技術(shù)比其它技術(shù)更好的人永遠(yuǎn)不會(huì)成為出色的產(chǎn)品:dog:。
From Reactive, More Than Reactive
文章來(lái)源:http://javakk.com/563.html
也歡迎大家關(guān)注我的公眾號(hào)【Java老K】獲取更多干貨
參考資料
全球架構(gòu)師峰會(huì)(第二天-淘寶應(yīng)用架構(gòu)升級(jí)——反應(yīng)式架構(gòu)的探索與實(shí)踐)
https://archsummit.infoq.cn/2019/shenzhen/schedule
Project Reactor:
https://projectreactor.io/
RxJava:
http://reactivex.io/
openjdk loom
jdk:http://jdk.java.net/loom/
wiki:https://wiki.openjdk.java.net/display/loom/Main
github:https://github.com/openjdk/loom