注:文章中的坑出現在2.5.4版本之前,這個坑在2.5.4版本已經得到修復。
一、問題描述
問題描述
場景描述,如上圖所示:
客戶端
遠程異步調用服務A
,服務A
在處理客戶端請求的過程中需要遠程同步調用服務B
,服務A
從服務B
的響應中取數據時,得到的是 null
!!!
二、原因分析
RPC請求響應參數傳遞過程
2.1 Client的請求發送過程
1)Client在發起RPC調用請求前,將請求參數構建成 RpcInvocation
;
2)Client在發起RPC調用請求前,會經過Filter處理:
-
ConsumerContextFilter
會將請求信息,如invoker、invocation、Address等,寫入RpcContext
;
@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
.setLocalAddress(NetUtils.getLocalHost(), 0)
.setRemoteAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(invoker);
}
try {
return invoker.invoke(invocation);
} finally {
RpcContext.getContext().clearAttachments();
}
}
}
3)Client在發起RPC調用請求前,會經過AbstractInvoker:
-
AbstractInvoker
會將RpcContext
中的attachments
內容寫入到RpcInvocation
,以實現附加參數的傳遞;
Map<String, String> context = RpcContext.getContext().getAttachments();
if (context != null) {
invocation.addAttachmentsIfAbsent(context);
}
-
AbstractInvoker
會從RPC請求參數URL
中ASYNC_KEY
的值,并設置到RpcInvocation
的attachment
中;
if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)) {
invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
}
4)Client在發起RPC調用請求時,會經過DubboInvoker:
- DubboInvoker會優先從
RpcInvocation
的attachment
中獲取并判斷ASYNC_KEY
是否為true,以實現消費端的異步調用;
public static boolean isAsync(URL url, Invocation inv) {
boolean isAsync;
//如果Java代碼中設置優先.
if (Boolean.TRUE.toString().equals(inv.getAttachment(Constants.ASYNC_KEY))) {
isAsync = true;
} else {
isAsync = url.getMethodParameter(getMethodName(inv), Constants.ASYNC_KEY, false);
}
return isAsync;
}
5)Client在發起RPC調用請求時,會將RpcInvocation
作為調用參數傳遞給服務提供方:
-
RpcInvocation
中的擴展屬性attachments
,實現了請求調用擴展信息傳遞的功能;
2.2 服務A的請求接收和執行過程
1)服務端在接收到RPC請求,調用真正實現接口前,會經過ContextFilter
。
-
ContextFilter
會將請求信息,如invoker、invocation、Address等,寫入RpcContext
; -
ContextFilter
會將請求參數RpcInvocation
的attachments
擴展信息取出,過濾掉某些特定KEY之后,將其余擴展屬性設置到當前RpcContext
的attachments
中;
Map<String, String> attachments = invocation.getAttachments();
if (attachments != null) {
attachments = new HashMap<String, String>(attachments);
attachments.remove(Constants.PATH_KEY);
attachments.remove(Constants.GROUP_KEY);
attachments.remove(Constants.VERSION_KEY);
attachments.remove(Constants.DUBBO_VERSION_KEY);
attachments.remove(Constants.TOKEN_KEY);
attachments.remove(Constants.TIMEOUT_KEY);
attachments.remove(Constants.ASYNC_KEY);//清空消費端的異步參數,2.5.4版本才新加進去的
}
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
.setAttachments(attachments)
.setLocalAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
其中attachments.remove(Constants.ASYNC_KEY);//清空消費端的異步參數
這行代碼是在dubbo的2.5.4版本才加進去的,也就是之前的版本中并沒有這行代碼。
2)在2.5.4版本之前,對于Client
發來的異步調用請求,其RpcInvocation
參數中包含了ASYNC=true
的attachment
擴展信息:
- 此時
ASYNC=true
的這個擴展信息就會被設置到服務A的RpcContext
的擴展屬性中; - 在
服務A
處理RPC調用,執行實際接口實現類的邏輯時,因為依賴的服務B
,所以會繼續發送RPC調用請求給服務B
; -
服務A
調用服務B
時,服務A
的RpcContext
的擴展屬性會被寫入到A -> B
的RpcInvocation
參數中,這就導致ASYNC=true的擴展屬性參數被誤傳到A -> B
的RpcInvocation
參數中,進而導致在服務A發起RPC請求調用時觸發了錯誤的異步調用邏輯; - 此時
服務A
獲取到的RPC執行結果RpcResult
的內容當然是個空;
else if (isAsync) { //1. 異步,有返回值
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
return new RpcResult();
} else { //3. 異步->同步(默認的通信方式)
RpcContext.getContext().setFuture(null);
return (Result) currentClient.request(inv, timeout).get();
}
以上就是這個坑的產生原因
三、解決方法
自己寫了個Filter,添加到Dubbo服務提供方接收請求后、實際處理請求前的Filter執行鏈中。
從請求參數URL
中解析出ASYNC
擴展參數標識,而不依賴RpcInvocation
中的值。
@Activate(group = {Constants.PROVIDER}, order = -999)
public class DubboSyncFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
//避免RpcContext透傳,使用配置文件的async
boolean isAsync = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false);
RpcContext.getContext().setAttachment(Constants.ASYNC_KEY, String.valueOf(isAsync));
return invoker.invoke(invocation);
}
}