acceptEither仅消费最先完成的结果,不兜底、不比较、不传播异常;双机热备需组合completeOnTimeout与exceptionally实现主备切换、超时控制和异常fallback。

acceptEither 不是为“合并结果”设计的,它只消费**最先完成的那个结果**,另一个会被静默丢弃。双机热备场景下若想实现“快速返回 + 容错兜底”,必须明确区分「主路径」和「备用路径」,不能依赖 acceptEither 自动选优或兜底。
acceptEither 的真实行为:谁快谁上,不等、不比、不兜底
它只在两个 CompletableFuture 中任一完成时触发回调,且仅传入该完成的结果;另一个即使后续成功或失败,也不会被感知,更不会触发异常传播或 fallback 逻辑。
- 常见错误现象:
acceptEither回调执行了,但备用服务其实也返回了更准确的结果(比如主服务缓存过期导致数据陈旧),你却完全不知道 - 使用场景:适合纯“竞速”类需求,如 DNS 解析多地址尝试、接口健康探活、灰度流量分流验证
- 性能影响:无额外开销,但无法控制哪个任务被选中 —— 取决于网络抖动、GC 暂停、线程调度等不可控因素
双机热备真正需要的是 acceptEither + exceptionally + completeOnTimeout 组合
单靠 acceptEither 无法满足热备的可靠性要求。必须叠加超时控制与异常兜底,才能确保:主服务慢/挂时自动切备用,主服务快时优先返回,主备都失败时有明确 fallback。
-
completeOnTimeout给主服务设一个合理阈值(如 300ms),超时则主动标记为“不可用”,让备用结果成为唯一可用选项 -
exceptionally捕获主服务抛出的异常(如ConnectException、TimeoutException),立即触发备用请求,而不是等它自然超时 - 备用请求本身也要加
completeOnTimeout,避免它也卡住导致整体阻塞 - 示例关键片段:
CompletableFuture<Result> primary = CompletableFuture.supplyAsync(() -> callPrimary(), executor) .orTimeout(300, TimeUnit.MILLISECONDS) .exceptionally(e -> { log.warn("Primary failed, fallback to standby", e); return null; }); <p>CompletableFuture<Result> standby = CompletableFuture.supplyAsync(() -> callStandby(), executor) .orTimeout(800, TimeUnit.MILLISECONDS);</p><p>primary.acceptEither(standby, result -> { if (result != null) { handle(result); // 注意:这里 result 可能来自主或备用,需业务层识别来源 } });
容易踩的坑:线程池混用、结果不可区分、状态丢失
热备逻辑一旦写错,轻则响应变慢,重则掩盖故障、返回脏数据。
- 别用
ForkJoinPool.commonPool():IO 密集型调用(如 HTTP)会迅速耗尽其默认线程数(availableProcessors() - 1),导致后续任务排队 —— 必须用自定义ThreadPoolExecutor,并设置合理 core/max 和队列策略 - 主备结果必须可区分:不要直接
acceptEither后就处理,要在回调里通过result.getSource()或包装类字段判断来源,否则无法做数据一致性校验或日志归因 -
acceptEither不传播异常:如果备用也失败,整个链路会静默终止,get()调用时才抛CompletionException—— 应该用whenComplete或handle显式检查Throwable参数
热备不是简单起两个请求扔给 acceptEither 就完事。真正的难点在于:如何定义“主失效”的边界(是超时?还是异常类型?)、如何让备用请求在主未失败时不提前启动(避免资源浪费)、以及结果到来后如何安全地中断仍在运行的另一个请求 —— 这些都需要手动编排,acceptEither 本身不提供任何支持。

















