Dubbo异步调用三种方式
从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
使用 CompletableFuture 签名的接口
需要服务提供者事先定义 CompletableFuture 签名的服务,具体参见服务端异步执行接口定义:
public interface AsyncService {
CompletableFuture<String> sayHello(String name);
}
服务端实现
public class AsyncServiceImpl implements AsyncService {
@Override
public CompletableFuture<String> sayHello(String name) {
RpcContext savedContext = RpcContext.getContext();
// 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
return CompletableFuture.supplyAsync(() -> {
System.out.println(savedContext.getAttachment("consumer-key1"));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "async response from provider.";
});
}
}
注意接口的返回类型是 CompletableFuture<String>
。
消费端XML引用服务:
<dubbo:reference id="asyncService" timeout="10000" interface="com.alibaba.dubbo.samples.async.api.AsyncService"/>
调用远程服务:
// 调用直接返回CompletableFuture
CompletableFuture<String> future = asyncService.sayHello("async call request");
// 增加回调
future.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("Response: " + v);
}
});
// 早于结果输出
System.out.println("Executed before response return.");
使用 RpcContext
在 consumer.xml 中配置:
<dubbo:reference id="asyncService" interface="org.apache.dubbo.samples.governance.api.AsyncService">
<dubbo:method name="sayHello" async="true" />
</dubbo:reference>
调用代码:
// 此调用会立即返回null
asyncService.sayHello("world");
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future
CompletableFuture<String> helloFuture = RpcContext.getContext().getCompletableFuture();
// 为Future添加回调
helloFuture.whenComplete((retValue, exception) -> {
if (exception == null) {
System.out.println(retValue);
} else {
exception.printStackTrace();
}
});
或者也可以这样做异步调用:
CompletableFuture<String> future = RpcContext.getContext().asyncCall(
() -> {
asyncService.sayHello("oneway call request1");
}
);
future.get();
重载服务接口
如果只有这样的同步服务定义,而又不喜欢 RpcContext 的异步使用方式。
public interface GreetingsService {
String sayHi(String name);
}
那还有一种方式,就是利用 Java 8 提供的 default 接口实现,重载一个带有 CompletableFuture 签名的方法。
有两种方式来实现:
- 提供方或消费方自己修改接口签名
public interface GreetingsService {
String sayHi(String name);
// AsyncSignal is totally optional, you can use any parameter type as long as java allows your to do that.
default CompletableFuture<String> sayHi(String name, AsyncSignal signal) {
return CompletableFuture.completedFuture(sayHi(name));
}
}
- Dubbo 官方提供 compiler hacker,编译期自动重写同步方法。
也可以设置是否等待消息发出:
-
sent="true"
等待消息发出,消息发送失败将抛出异常。 -
sent="false"
不等待消息发出,将消息放入 IO 队列,即刻返回。
<dubbo:method name="findFoo" async="true" sent="true" />
如果只是想异步,完全忽略返回值,可以配置 return="false"
,以减少 Future 对象的创建和管理成本:
<dubbo:method name="findFoo" async="true" return="false" />
商详中的Dubbo异步应用
一个商详页的数据需要由多个接口返回的数据组装而成,如果每个接口都以串行的方式去执行,那么整个商详接口的RT将大大增长;
而商详接口中,我们把依赖的接口每个都单独封装成DataLoader,下面是商详的数据加载模型;
首先,服务启动的时候会把不同的DataLoader进行分类:
具体有哪些DataLoader可以看末尾参考资料;
class:asynccall.DataLoaderRule#init
public void init() {
int order = 0 ;
for (DataLoader dataLoader : dataLoaders) {
if (dataLoader instanceof AsyncDataLoader) {
asyncDataLoaders.add((AsyncDataLoader) dataLoader);
} else {
syncDataLoaders.add(dataLoader);
}
// 静态化,不包含用户信息,可缓存于CDN
if (!(dataLoader instanceof HelpCutDataLoader || dataLoader instanceof LuckyDrawGroupDataLoader
|| dataLoader instanceof PromotionDataLoader
|| dataLoader instanceof PurchaseRightDataLoader || dataLoader instanceof SeckillDataLoader
|| dataLoader instanceof ShopLifecycleDataLoader || dataLoader instanceof UserBuyLimitLoader
|| dataLoader instanceof ItemExistUnPayStockNumLoader || dataLoader instanceof GoodsSmartTextDataLoader)) {
staticDataLoaders.add((AsyncDataLoader) dataLoader);
}
// 用户态数据执行计划
if (dataLoader instanceof HelpCutDataLoader || dataLoader instanceof LuckyDrawGroupDataLoader
|| dataLoader instanceof PromotionDataLoader
|| dataLoader instanceof PurchaseRightDataLoader || dataLoader instanceof SeckillDataLoader
|| dataLoader instanceof ItemSkuDataLoader || dataLoader instanceof ShopConfigsDataLoader
|| dataLoader instanceof UserBuyLimitLoader || dataLoader instanceof ItemExistUnPayStockNumLoader
|| dataLoader instanceof ChainItemSkuDataLoader || dataLoader instanceof PhysicalStoreDataLoader) {
userStateDataLoaders.add((AsyncDataLoader) dataLoader);
}
if (dataLoader instanceof HelpCutDataLoader || dataLoader instanceof LuckyDrawGroupDataLoader
|| dataLoader instanceof PromotionDataLoader || dataLoader instanceof SeckillDataLoader
|| dataLoader instanceof ItemSkuDataLoader || dataLoader instanceof ShopConfigsDataLoader
|| dataLoader instanceof ShopAggrDataLoader || dataLoader instanceof ChainItemSkuDataLoader
|| dataLoader instanceof GoodsSmartTextDataLoader) {
posterDataLoaders.add((AsyncDataLoader) dataLoader);
}
if (dataLoader instanceof VoucherLoader) {
vouchersDataLoaders.add((AsyncDataLoader) dataLoader);
}
}
}
当访问静态商详页接口时:
// 静态商详接口
public ItemStaticAggregateModel getStaticInfo(ItemStaticInfoGetParam param){
// 第一步: 信息获取
ResFutureContext ctx = dataLoaderResolver.getContext(param);
// 第二步: 信息组装
return doBuild(ctx);
}
在 dataLoaderResolver.getContext 方法中,塞入了业务标识"detail-static":
class:com.xxx.item.detail.biz.asynccall.DataLoaderResolver#getContext(com.xxx.item.detail.api.param.ItemStaticInfoGetParam)
/**
* 基本数据上下文
*/
public ResFutureContext getContext(ItemStaticInfoGetParam param) {
ResFutureContext ctx = new ResFutureContext(param);
// 设置业务身份
ctx.putBizIdentity(BizIdentityKeys.SCENE_ID, "detail-static");
ItemDetailModel itemDetail = buildItemDetailForStatic(param.getAlias(),!Boolean.TRUE.equals(param.getNotQueryXxxGuarantee()));
ctx.put(AttributeConstant.ITEM_BASE_INFO, itemDetail);
// 异步加载数据
return asynLoad(ctx);
}
private ResFutureContext asynLoad(ResFutureContext ctx){
for (AsyncDataLoader asyncDataLoader : dataLoaderRule.getAsyncLoaderRules(ctx)) {
if (asyncDataLoader.loadable(ctx)) {
asyncDataLoader.load(ctx);
}
}
return ctx;
}
在 asynLoad 方法中,dataLoaderRule.getAsyncLoaderRules 方法里会根据业务标识返回 staticDataLoaders:
class:com.xxx.item.detail.biz.asynccall.DataLoaderRule#getAsyncLoaderRules
/**
* 根据数据加载规则获取加载计划
*/
public List<AsyncDataLoader> getAsyncLoaderRules(ResFutureContext ctx) {
if (Objects.equals(ctx.getBizIdentity(BizIdentityKeys.SCENE_ID), "seckill")) {
return getAsyncLoaderRulesForSeckill();
}
// 静态化加载计划
if (Objects.equals(ctx.getBizIdentity(BizIdentityKeys.SCENE_ID), "detail-static")) {
return staticDataLoaders;
}
if (Objects.equals(ctx.getBizIdentity(BizIdentityKeys.SCENE_ID), "detail-poster")) {
return posterDataLoaders;
}
if (Objects.equals(ctx.getBizIdentity(BizIdentityKeys.SCENE_ID), "detail-vouchers")) {
return vouchersDataLoaders;
}
if(Objects.equals(ctx.getBizIdentity(BizIdentityKeys.SCENE_ID), "detail-user")){
return getAsyncUserDataLoaders(ctx);
}
// 默认数据规则
return asyncDataLoaders;
}
以ItemSkuDataLoader为例:
ItemSkuDataLoader class:com.xxx.item.detail.biz..asynccall.loader.itemdata.ItemSkuDataLoader
SimpleAsyncDataLoader class:com.xxx.item.detail.biz..asynccall.loader.SimpleAsyncDataLoader
asyncDataLoader.load(ctx) 方法会进入到父类 SimpleAsyncDataLoader#load
public Result<T> load(ResFutureContext ctx) {
// 尝试从 ctx 缓存获取,防止一次静态接口访问中重复请求itemSku接口
Result<T> result = loadFromCache(ctx);
if (result != null) {
return result;
}
Class<? extends SimpleAsyncDataLoader> clz = this.getClass();
FusingConfig fc = FusingFactory.getMethodConfig(clz.getName());
// 是否开启降级
if (fc != null && fc.isForcingFallBack()) {
Method fallBackMethod = getFallBackMethod(clz);
try {
if (null != fallBackMethod) {
result = invokeFallback(fallBackMethod, ctx, null);
}
} catch (ReflectiveOperationException e) {
LOGGER.error("invoke fallback method error" + clz.getName(), e);
throw new ItemException("invoke fallback method error" + clz.getName());
}
} else {
// 直接看这里,调用子类 doLoad 方法
result = doLoad(ctx);
setFallbackMethodIfFallbackable(ctx, result);
setTimeout(result,fc);
}
// 返回结果放入 ctx 中
saveToCache(ctx, result);
return result;
}
SimpleAsyncDataLoader#doLoad 方法会进入 RpcContextHelper#asyncCall
public class ItemSkuDataLoader extends SimpleAsyncDataLoader<List<ItemSkuModel>> {
public static final Logger logger = LoggerFactory.getLogger(ItemSkuDataLoader.class);
@Resource
private ItemSkuInfoQueryService itemSkuInfoQueryServiceClient;
@Resource
private DateSkuQueryService dateSkuQueryService;
@Override
public boolean loadable(ResFutureContext ctx) {
ItemDetailModel itemDetailModel = ctx.getItemDetailModel();
return !Objects.equals(itemDetailModel.getBuyWay(), 0);
}
@Override
protected Result<List<ItemSkuModel>> doLoad(ResFutureContext ctx) {
ItemDetailBaseGetParam param = ctx.getParam();
ItemDetailModel itemDetailModel = ctx.getItemDetailModel();
ListByItemIdQueryParam queryParam = new ListByItemIdQueryParam();
queryParam.setItemId(itemDetailModel.getId());
queryParam.setKdtId(itemDetailModel.getKdtId());
queryParam.setOfflineId(param.getStoreId());
queryParam.setUseCache(true);
//优惠券商品需要查询sku标上关联的优惠券
if (ItemTypeUtils.isPayCoupon(itemDetailModel)) {
queryParam.setWithSkuMark(true);
}
return RpcContextHelper.asyncCall(
() -> itemSkuInfoQueryServiceClient.listByItemId(queryParam),
(result) -> {
if (!result.isSuccess()) {
logger.error("接口 SC ItemSkuService.listItemSkusIncludeFenxiao 调用失败!params:{}, error result:{}",
JSON.toJSONString(queryParam), JSON.toJSONString(result));
throw new ItemException(result);
}
List<ItemSkuModel> skuModels = ItemSkuProxy
.toItemSkuInfoNewModel(result.getData());
// 修正分销sku库存数据
if(! ItemTypeUtils.isFenxiao(itemDetailModel.getGoodsType(), itemDetailModel.getItemMarkAggregateModel())) {
// 电子卡券且开启价格日历,检查在一定时间内是否有可售的sku,若有,则认为所有sku都可售;若没有则认为库存都为0
processEcardCalendarSkuStockNum(ctx.getItemDetailModel(), param.getSkuCalendarEndTime(), skuModels);
return skuModels;
}
if(this.needRemoveUnSellSku(ctx)){
List<ItemSkuModel> resultSku = ItemSkuProxy.processFenxiaoSkuRemoveUnSellSku(ctx.getItemDetailModel(),skuModels);
if (resultSku.size() == 1 && !resultSku.get(0).canDeduct()){
resultSku.get(0).setStockNum(0L);
}
return resultSku;
}
ItemSkuProxy.processFenxiaoSku(skuModels);
return skuModels;
});
}
}
RpcContextHelper#asyncCall 返回的是 FutureRes
RpcContextHelper class:com.xxx.item.detail.biz..asynccall.RpcContextHelper#asyncCall
public static <T, R> Result<R> asyncCall(Callable<T> callable, FutureResParser<T, R> parser) {
Future<T> future = RpcContext.getContext().asyncCall(callable);
return new FutureRes<>(future, parser);
}
最终返回的FutureRes会放入 ctx 的 container 中,container是个CHM,key为每个DataLoader的className;
接着看 RpcContext.getContext().asyncCall(callable):
class:com.alibaba.dubbo.rpc.RpcContext#asyncCall(java.util.concurrent.Callable<T>)
public <T> Future<T> asyncCall(Callable<T> callable) {
try {
try {
// 设置了异步调用的 标识
setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
// Dubbo调用,最终会进入到 DubboInvoker
final T o = callable.call();
//local调用会直接返回结果.
if (o != null) {
FutureTask<T> f = new FutureTask<T>(new Callable<T>() {
public T call() throws Exception {
return o;
}
});
f.run();
return f;
} else {
}
} catch (Exception e) {
throw new RpcException(e);
} finally {
removeAttachment(Constants.ASYNC_KEY);
}
} catch (final RpcException e) {
return new Future<T>() {
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
public boolean isCancelled() {
return false;
}
public boolean isDone() {
return true;
}
public T get() throws InterruptedException, ExecutionException {
throw new ExecutionException(e.getCause());
}
public T get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException,
TimeoutException {
return get();
}
};
}
return ((Future<T>)getContext().getFuture());
}
Dubbo的调用最终都会走到 DubboInvoker#doInvoke 中,然后再从 DubboInvoker 发起远程调用,异步调用就是在这个类中区分开的;
公司使用的是 alibaba 版本的Dubbo;
class:com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker
protected Result doInvoke(Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation)invocation;
String methodName = RpcUtils.getMethodName(invocation);
inv.setAttachment("path", this.getUrl().getPath());
inv.setAttachment("version", this.version);
ExchangeClient currentClient;
if (this.clients.length == 1) {
currentClient = this.clients[0];
} else {
currentClient = this.clients[this.index.getAndIncrement() % this.clients.length];
}
try {
// 根据之前在RpcContent设置的参数判断是否为异步调用
boolean isAsync = RpcUtils.isAsync(this.getUrl(), invocation);
// 是否有返回值
boolean isOneway = RpcUtils.isOneway(this.getUrl(), invocation);
int timeout = this.getUrl().getMethodParameter(methodName, "timeout", 1000);
if (isOneway) {
boolean isSent = this.getUrl().getMethodParameter(methodName, "sent", false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture((Future)null);
return new RpcResult();
} else if (isAsync) {
// 走这里发送 RPC 请求
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter(future));
return new RpcResult();
} else {
RpcContext.getContext().setFuture((Future)null);
return (Result)currentClient.request(inv, timeout).get();
}
} catch (TimeoutException var9) {
// 异常输出
}
}
返回是个封装了 channel 的 DefaultFuture,里面使用了用了 ReentrantLock 做了get RPC结果的阻塞;
class:com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int)
public ResponseFuture request(Object request, int timeout) throws RemotingException {
if (closed) {
throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
}
// create request.
Request req = new Request();
req.setVersion("2.0.0");
req.setTwoWay(true);
req.setData(request);
// 把 channel 返回,当调用get时返回
DefaultFuture future = new DefaultFuture(channel, req, timeout);
try{
channel.send(req);
}catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
apache 2.7.0 版本开始 DubboInvoker 默认就是异步调用
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
inv.setAttachment(PATH_KEY, getUrl().getPath());
inv.setAttachment(VERSION_KEY, version);
ExchangeClient currentClient;
if (clients.length == 1) {
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = calculateTimeout(invocation, methodName);
invocation.put(TIMEOUT_KEY, timeout);
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else {
ExecutorService executor = getCallbackExecutor(getUrl(), inv);
CompletableFuture<AppResponse> appResponseFuture =
currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
// save for 2.6.x compatibility, for example, TraceFilter in Zipkin uses com.alibaba.xxx.FutureAdapter
FutureContext.getContext().setCompatibleFuture(appResponseFuture);
AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
result.setExecutor(executor);
return result;
}
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
返回来看商详数据组装部分:
然后会在各个 buildXXX() 方法中从 ctx 获取 FutureRes(FutureTask(DefaultFuture())),实际上是调用了底层 DefaultFuture 的 get 方法;
class:com.xxx.item.detail.biz.inner.impl.ItemDetailInnerServiceImpl#doBuild
private ItemStaticAggregateModel doBuild(ResFutureContext ctx){
ItemDetailBuilderV2 itemDetailBuilderV2 = itemDetailBuilderV2Factory.get(ctx);
// step1: 构建基础商品信息
itemDetailBuilderV2.buildItemBasicModel();
// step2: 构建基础商品状态信息:类型、行业扩展信息等
itemDetailBuilderV2.buildItemStatusModel();
// step3: 构建规格信息
itemDetailBuilderV2.buildSkus();
// step4: 构建店铺信息
itemDetailBuilderV2.buildShopInformation();
// step5: 构建店铺配置信息
itemDetailBuilderV2.buildShopConfigModel();
// step6: 构建物流信息
itemDetailBuilderV2.buildDelivery();
// step7: 构建优惠券信息
itemDetailBuilderV2.buildVoucherModel();
// step8: 构建商品页面配置信息
itemDetailBuilderV2.buildPageFeatureConfigModel();
// step9: 构建商品扩展信息
itemDetailBuilderV2.buildExtendModel();
return postProcessAfterBuildV2(ctx, itemDetailBuilderV2.getItemStaticAggregateModel());
}