有用过 Dubbo 的异步调用吗?
结论先行:用过。Dubbo 的异步调用分三代:2.6 的
async=true配RpcContext.getFuture(),2.7 起getFuture()返回CompletableFuture,再往后推荐接口直接返回CompletableFuture。本质是把“调用线程阻塞等响应”改成“调用发出后立刻返回,响应到达时回调”,用少量线程撑住高并发。
一、三种写法
// ① 2.6 时代:显式 async + Future
@DubboReference(async = true)
private UserService userService;
public void oldStyle() {
userService.getById(1L);
Future<Object> f = RpcContext.getContext().getFuture(); // 拿上一次调用的 Future
// ...
}
// ② 2.7+:getFuture() 返回 CompletableFuture,可组合
@DubboReference(async = true)
private UserService userService;
public void futureStyle() {
userService.getById(1L); // 立即返回 null
CompletableFuture<Object> f = RpcContext.getContext().getFuture();
f.thenAccept(this::handle).exceptionally(e -> {
log.error("async call failed", e);
return null;
});
}
// ③ 推荐:接口直接返回 CompletableFuture
public interface UserService {
CompletableFuture<User> getByIdAsync(Long id);
}
二、同步 vs 异步的线程账
同步调用(200 线程池,单次 50ms,目标 10k QPS)
需要 10000 × 0.05 = 500 个线程 → 超池 → 排队 → 雪崩
异步调用(50 个线程发起,响应回调处理)
发起即返回,线程可复用 → 线程数与并发解耦
| 维度 | 同步 | 异步 |
|---|---|---|
| 线程占用 | 与在途请求数成正比 | 与 CPU 核数相关 |
| 编程复杂度 | 低 | 高(回调地狱) |
| 错误处理 | try-catch | 回调里判断,或 exceptionally |
| 上下文传递 | ThreadLocal 直接可用 | 需显式透传,线程已切换 |
| 超时语义 | 阻塞到超时 | 超时后 Future 异常完成 |
三、组合与超时
CompletableFuture<User> fu = userService.getByIdAsync(1L);
CompletableFuture<List<Order>> fo = orderService.listByUserAsync(1L);
// 并行聚合,整体 800ms 超时
CompletableFuture<UserDetail> detail =
fu.thenCombine(fo, (u, os) -> new UserDetail(u, os))
.orTimeout(800, TimeUnit.MILLISECONDS)
.exceptionally(e -> UserDetail.empty());
orTimeout 只让 Future 失败,不会取消已在执行的服务端请求,写操作仍要靠业务幂等或状态机兜底。
四、什么时候不该用异步
- 逻辑本身有强先后依赖,异步化只是把阻塞从线程搬到回调,收益很小。
- 链路调用深度大且每层都异步,排障成本陡增,建议只在扇出(fan-out)场景用。
- 事务边界内不要异步化,提交与回滚在另一个线程上,事务上下文会丢。
常见追问
- 追问:异步调用后 ThreadLocal 里的租户 ID 还在吗?不在,回调可能在其他线程执行,要用
RpcContext的 attachment 显式透传,或包装 executor 做上下文复制。 - 追问:
RpcContext是线程安全的吗?不是,它绑定当前线程,异步回调里要重新设置。 - 追问:异步会不会导致请求堆积?会。没有背压时发送速度超过响应速度,内存里会堆积大量未完成 Future,需要配
actives限流。
小结
- 异步把线程占用与并发量解耦,适合扇出、批量聚合、网关类场景。
- 三种写法中优先用返回
CompletableFuture的接口,显式且类型安全。 - 超时只影响 Future,不取消服务端执行,写操作必须幂等。
- 上下文与事务不会自动跨线程,异步化的隐性成本主要在这一点。