同步調(diào)用的缺點(diǎn)
我們假設(shè)一個電子商城用戶購買商品的場景: 創(chuàng)建訂單前的驗(yàn)證方法砾跃。
/**
* 驗(yàn)證訂單是否合法
*
* @param userId 用戶id
* @param itemId 商品id
* @param discount 折扣
* @return
*/
public boolean verifyOrder(long userId, long itemId, double discount) {
// 驗(yàn)證用戶能否享受這一折扣盹牧,RPC調(diào)用
boolean verifyDiscount = discountService.verify(userId, itemId, discount);
if(!verifyDiscount) {
// 該用戶無法享受這一折扣
return false;
}
// 獲取商品單價加派,RPC調(diào)用
double itemPrice = storeService.getPrice(itemId);
// 用戶實(shí)際應(yīng)該支付的價格
double realPrice = itemPrice * discount;
// 獲取用戶賬號余額熟尉,限定了只能使用余額購買熏版,RPC調(diào)用
double balance = userService.getBalance(userId);
return realPrice <= balance;
}
這個方法里面涉及到了 3 個 rpc 調(diào)用纷责,假設(shè)每個 rpc 調(diào)用都需要 10ms,那么
verifyOrder 這個方法總耗時將不低于 30ms撼短。
在同步調(diào)用系統(tǒng)中再膳,延遲同時會導(dǎo)致吞吐量的下降。如果只有一個線程曲横,那么系統(tǒng)每秒的吞吐量將不會高于 1000ms / 30ms喂柒,也就是最多 33 qps。同步系統(tǒng)要提高吞吐量禾嫉,唯一的辦法就是加大線程數(shù)灾杰。同時啟用 1,000 個線程,吞吐量理論值可以上升到 33,333 qps熙参。不過實(shí)際使用中艳吠,這并不是完美的方案:增加線程數(shù)量會導(dǎo)致頻繁的上下文切換,系統(tǒng)整體性能將會嚴(yán)重下降孽椰。
Future 的不足
為了解決同步系統(tǒng)的問題昭娩,Java 5 引入了 Future。有了 Future 后黍匾,上面的方法可以修改為:
/**
* 驗(yàn)證訂單是否合法
*
* @param userId 用戶id
* @param itemId 商品id
* @param discount 折扣
* @return
*/
public boolean verifyOrder(long userId, long itemId, double discount) {
// 驗(yàn)證用戶能否享受這一折扣栏渺,RPC調(diào)用
Future<Boolean> verifyDiscountFuture = discountService.verify(userId, itemId, discount);
// 獲取商品單價,RPC調(diào)用
Future<Double> itemPriceFuture = storeService.getPrice(itemId);
// 獲取用戶賬號余額锐涯,限定了只能使用余額購買磕诊,RPC調(diào)用
Future<Double> balanceFuture = userService.getBalance(userId);
if(!verifyDiscountFuture.get()) {
// 該用戶無法享受這一折扣
return false;
}
// 用戶實(shí)際應(yīng)該支付的價格
double realPrice = itemPriceFuture.get() * discount;
// 用戶賬號余額
double balance = balanceFuture.get();
return realPrice <= balance;
}
3 個 rpc 調(diào)用可以同時進(jìn)行了,系統(tǒng)延遲降低為之前的 1/3纹腌。不過延遲降低吞吐量的問題還是沒有解決霎终,依然需要通過增加線程數(shù)來提升吞吐量。
CompletableFuture 才是王道
引入 CompletableFuture 后升薯,我們可以使用如下形式:
/**
* 驗(yàn)證訂單是否合法
*
* @param userId 用戶id
* @param itemId 商品id
* @param discount 折扣
* @return
*/
public CompletableFuture<Boolean> verifyOrder(long userId, long itemId, double discount) {
// 驗(yàn)證用戶能否享受這一折扣莱褒,RPC調(diào)用
CompletableFuture<Boolean> verifyDiscountFuture = discountService.verify(userId, itemId, discount);
// 獲取商品單價,RPC調(diào)用
CompletableFuture<Double> itemPriceFuture = storeService.getPrice(itemId);
// 獲取用戶賬號余額覆劈,限定了只能使用余額購買保礼,RPC調(diào)用
CompletableFuture<Double> balanceFuture = userService.getBalance(userId);
return CompletableFuture
.allOf(verifyDiscountFuture, itemPriceFuture, balanceFuture)
.thenApply(v -> {
if(!verifyDiscountFuture.get()) {
// 該用戶無法享受這一折扣
return false;
}
// 用戶實(shí)際應(yīng)該支付的價格
double realPrice = itemPriceFuture.get() * discount;
// 用戶賬號余額
double balance = balanceFuture.get();
return realPrice <= balance;
});
}
延遲降低為原來 1/3,同時吞吐量也不會因?yàn)檠舆t而降低责语。非常完美炮障,簡單高效,CompletableFuture 絕對稱得上是大殺器坤候。在 rpc 異步調(diào)用這個問題上胁赢,沒什么比 CompletableFuture 更適合的解決方案了。CompletableFuture 是 Doug Lea 的又一力作白筹,徹底解決了 Future 的缺陷智末,把 Java 帶入了異步響應(yīng)式編程的新世界。
motan dubbo 的解決方案是否完美
不完美徒河,會陷入回調(diào)地獄系馆,motan 和 dubbo 的設(shè)計(jì)者應(yīng)該考慮引入 CompletableFuture 了。
Akka RxJava Reactor 是否可用顽照?
這 3 個方案都能完美解決上述問題由蘑。但我個人認(rèn)為這 3 個方案都有一些不足之處,不過這非常帶有主觀性偏見代兵,僅供讀者參考尼酿。后面我可能會從 代碼量 代碼復(fù)雜度 性能 等方面給出更全面客觀評測。
Akka
從來沒見過這樣復(fù)雜的系統(tǒng)植影,我覺得我搞不定它裳擎。actor 機(jī)制盡管有一堆的優(yōu)點(diǎn),但在我看來這東西就是新形式的 goto思币。跳來跳去悠忽不定鹿响,簡直就是系統(tǒng)維護(hù)的地獄,完全搞不定它支救。RxJava 和 Reactor
沒什么大問題抢野,主要是引入了很多新的概念。要使用他們需要先花一段時間研究各墨,這對在團(tuán)隊(duì)中推廣很不利指孤。不過如果你的團(tuán)隊(duì)對這兩個中的任意一個已經(jīng)有過深入研究的話,我覺得用起來完全沒問題贬堵。
廣告時間
Turbo 采用 CompletableFuture 作為異步解決方案恃轩,性能非常好。
更多內(nèi)容詳見:
https://github.com/hank-whu/turbo-rpc