Skip to content
Xiaoyu's Blog
Go back

如何优雅地 Cancel:从 CompletableFuture.allOf() 看 Java 与 Go 的取消机制

最近在设计一个 BFF 聚合接口时,我遇到了一个看似很简单的问题。

假设首页需要同时查询:

于是使用 CompletableFuture 并发调用:

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userService.queryUser(userId));

CompletableFuture<List<Post>> postFuture =
        CompletableFuture.supplyAsync(() -> postService.queryPosts(userId));

CompletableFuture<List<Activity>> activityFuture =
        CompletableFuture.supplyAsync(() -> activityService.queryActivities());

CompletableFuture.allOf(
        userFuture,
        postFuture,
        activityFuture
).join();

这时候突然想到一个问题:

如果其中一个任务已经超时,整个请求已经没有继续执行的意义,那么应该如何取消剩下的任务?

第一反应可能是:

userFuture.cancel(true);
postFuture.cancel(true);
activityFuture.cancel(true);

看起来非常合理。

但继续往下思考,就会发现一个很重要的问题:

CompletableFuture 被 cancel 以后,里面正在执行的 HTTP、Dubbo、Redis、MySQL 请求,真的被取消了吗?

答案是:

不一定。

而这件事情背后,其实涉及:

CompletableFuture
Thread Interrupt
Blocking IO
HTTP
RPC
Redis
MySQL
Go Context
Deadline Propagation
Resource Governance

本文就从这个问题一路往下分析。

Table of contents

Open Table of contents

1. CompletableFuture.cancel() 到底取消了什么?

先来看最简单的代码:

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> {
            return queryRemoteService();
        });

future.cancel(true);

很多人看到:

cancel(true)

会自然认为:

true 代表中断正在执行任务的线程。

但对于 CompletableFuture 来说,这个理解并不完全正确。

它最核心的作用是:

CompletableFuture
        ↓
状态变成 Cancelled
        ↓
get() / join()
        ↓
CancellationException

可以画成:

flowchart TD
    A["future.cancel(true)"] --> B["CompletableFuture 状态变为 Cancelled"]
    B --> C["后续 get / join"]
    C --> D["CancellationException"]

    A -. 不一定 .-> E["底层线程停止"]
    A -. 不一定 .-> F["HTTP 请求停止"]
    A -. 不一定 .-> G["SQL 停止执行"]

也就是说:

Future 被取消,不代表任务本身一定停止。

更不代表:

任务里面调用的远程资源已经停止。

例如:

CompletableFuture<Void> future =
        CompletableFuture.runAsync(() -> {
            while (true) {
                doSomething();
            }
        });

future.cancel(true);

即使:

future.isCancelled() == true

里面的循环仍然可能继续执行。

所以我们先得到第一个结论:

取消 Future ≠ 取消任务 ≠ 取消底层资源。


2. CompletableFuture 本质上只是任务组合工具

我们经常把:

CompletableFuture

理解为一个完整的异步任务框架。

但更准确地说:

CompletableFuture 更擅长解决的是异步任务的组合问题,而不是底层资源生命周期问题。

例如:

CompletableFuture<User> userFuture = queryUser();

CompletableFuture<List<Post>> postFuture = queryPosts();

return userFuture.thenCombine(
        postFuture,
        HomeVO::new
);

它非常擅长:

任务 A
任务 B
任务 C
   ↓
组合
   ↓
转换
   ↓
聚合

但是它不知道:

任务 A 里面是 HTTP
任务 B 里面是 Redis
任务 C 里面是 MySQL

因此它也没有能力自动完成:

future.cancel()
        ↓
HTTP Call.cancel()

future.cancel()
        ↓
JDBC Statement.cancel()

future.cancel()
        ↓
Dubbo RPC Cancel

这些事情。


3. 如果 CompletableFuture 里面是 HTTP 呢?

假设:

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(() -> {
            return httpClient.query();
        });

实际执行链路是:

flowchart LR
    A["CompletableFuture"] --> B["Worker Thread"]
    B --> C["HTTP Client"]
    C --> D["Socket"]
    D --> E["Network"]
    E --> F["Remote Server"]

如果此时:

future.cancel(true);

很可能只是:

CompletableFuture:
“这个结果我不要了。”

但 HTTP 请求可能还在继续。

所以 HTTP Client 本身必须拥有:


4. HTTP 推荐:OkHttp

如果是同步 Java 微服务,我比较推荐 OkHttp。

例如:

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(Duration.ofMillis(300))
        .readTimeout(Duration.ofSeconds(2))
        .writeTimeout(Duration.ofSeconds(2))
        .callTimeout(Duration.ofSeconds(3))
        .build();

这里最值得关注的是:

callTimeout()

它约束的是整个 HTTP Call 的生命周期。

同时 OkHttp 有一个非常重要的对象:

Call call = client.newCall(request);

它可以主动:

call.cancel();

因此可以把 HTTP 调用和 Future 分开理解:

flowchart TD
    A["CompletableFuture Timeout"] --> B["业务层不再等待结果"]

    C["OkHttp callTimeout"] --> D["HTTP 请求达到生命周期上限"]

    E["Call.cancel()"] --> F["主动取消当前 HTTP 请求"]

    B -. 不等于 .-> F

所以:

CompletableFuture
负责:
“我要不要继续等待结果”

OkHttp
负责:
“这个网络请求还能不能继续”

这是两个不同的职责。


5. Dubbo timeout 也是同样的问题

例如:

@DubboReference(timeout = 300)
private UserService userService;

然后:

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(
                () -> userService.getUser(userId)
        );

如果外面:

userFuture.cancel(true);

并不能简单理解为:

Dubbo RPC 被真正取消

RPC 生命周期真正应该依赖:

@DubboReference(timeout = 300)

因此建议分成:

CompletableFuture timeout
        ↓
控制聚合等待

Dubbo timeout
        ↓
控制 RPC 等待

但是这里还有一个更深的问题。

假设:

flowchart LR
    A["BFF"] --> B["Dubbo Consumer"]
    B --> C["Dubbo Provider"]
    C --> D["Service"]
    D --> E["MySQL"]

如果 BFF 已经超时:

BFF:
“不等了。”

Provider 端可能仍然在:

userMapper.selectById(userId);

甚至 MySQL 里的 SQL 仍然在执行。

所以:

Consumer timeout ≠ Provider 业务立即停止。


6. MySQL 是最典型的例子

例如:

CompletableFuture<User> future =
        CompletableFuture.supplyAsync(() ->
                jdbcTemplate.queryForObject(
                        sql,
                        User.class
                )
        );

虽然外面是:

CompletableFuture

但内部其实还是:

flowchart LR
    A["Worker Thread"] --> B["JDBC"]
    B --> C["Socket"]
    C --> D["MySQL"]
    D --> E["SQL Execution"]

传统 JDBC 本质上是 Blocking IO。

所以:

future.cancel(true);

不代表:

MySQL 里的 SQL 消失了

数据库层应该自己设置:

statement.setQueryTimeout(2);

或者在支持的场景调用:

statement.cancel();

所以:

CompletableFuture Cancel

和:

Database Query Cancel

完全是两个层面的事情。

可以理解成:

flowchart TD
    A["Future Cancel"] --> B["业务层放弃结果"]

    C["Statement.cancel"] --> D["JDBC Driver"]
    D --> E["通知 MySQL 取消 Query"]

    B -. 不等于 .-> E

7. Redis 也是一样

例如:

CompletableFuture<String> redisFuture =
        CompletableFuture.supplyAsync(() ->
                redisTemplate.opsForValue().get(key)
        );

Redis 调用链:

flowchart LR
    A["Java"] --> B["Lettuce"]
    B --> C["Netty"]
    C --> D["Socket"]
    D --> E["Redis Server"]

Future 被取消之后,并不意味着已经发送给 Redis 的命令自动消失。

所以 Redis Client 本身同样需要:

Connect Timeout
Command Timeout

如果使用 Spring Boot,我更倾向:

Spring Data Redis
        +
Lettuce

而不是试图:

CompletableFuture.cancel()

去承担 Redis 的生命周期管理。


8. 那是不是把线程停掉就可以?

这是另一个非常容易产生的想法。

既然任务最终是在一个 Thread 里面执行:

CompletableFuture.supplyAsync(...)

那是不是只需要:

把 Thread 停掉

就够了?

答案依然是:

不够。

因为一次远程调用占用的资源远远不只有线程和 CPU。

例如 HTTP:

TCP Connection
HTTP/2 Stream
Socket Buffer
Connection Pool
Kernel Buffer
Remote Server Thread
Remote Server CPU

数据库还可能占:

DB Connection
数据库 CPU
Lock
Buffer Pool
Temporary Table
Disk IO

所以一个非常危险的情况是:

上游已经不要结果了
        ↓
但是下游仍然继续执行

9. 不彻底取消,会造成什么问题?

假设:

QPS = 5000

某个下游因为故障,每个调用需要:

5 秒

才能真正结束。

理论上的在途请求数量:

5000 × 5
=
25000

也就是说可能同时存在:

25000 个下游请求

即使你的 BFF:

500ms

就已经返回 Timeout。

这些请求仍可能继续占:

Socket
Connection
DB Connection
RPC Thread
Memory
Remote CPU
Lock

最终产生:

flowchart TD
    A["下游变慢"] --> B["大量请求堆积"]
    B --> C["线程池耗尽"]
    B --> D["连接池耗尽"]
    B --> E["Socket 增多"]
    B --> F["DB Connection 耗尽"]

    C --> G["新请求无法执行"]
    D --> G
    E --> G
    F --> G

    G --> H["系统雪崩"]

这也是为什么 Timeout 和 Cancellation 不只是:

用户体验问题。

它更是:

资源治理问题。


10. Go 为什么在这个问题上更优雅?

然后我们来看 Go。

Go 里大量 API 都会接受:

context.Context

例如:

func QueryUser(
    ctx context.Context,
    userID int64,
) (*User, error) {
}

创建 Timeout:

ctx, cancel := context.WithTimeout(
    context.Background(),
    500*time.Millisecond,
)

defer cancel()

500ms 后:

ctx.Done()

会被关闭。

也可以主动调用:

cancel()

所以:

WithTimeout
    ↓
超时自动 cancel

WithCancel
    ↓
主动 cancel

最终都会变成:

ctx.Done()

11. Go Context 可以一路往下传播

这是 Context 真正漂亮的地方。

例如:

func Handler(
    w http.ResponseWriter,
    r *http.Request,
) {
    ctx, cancel :=
        context.WithTimeout(
            r.Context(),
            500*time.Millisecond,
        )

    defer cancel()

    user, err :=
        userService.Query(
            ctx,
            userID,
        )

    // ...
}

Service:

func (s *UserService) Query(
    ctx context.Context,
    userID int64,
) (*User, error) {

    return s.repository.Query(
        ctx,
        userID,
    )
}

DB:

db.QueryContext(
    ctx,
    sql,
    userID,
)

RPC:

grpcClient.GetUser(
    ctx,
    request,
)

HTTP:

request, err :=
    http.NewRequestWithContext(
        ctx,
        http.MethodGet,
        url,
        nil,
    )

于是:

flowchart TD
    A["HTTP Handler"] --> B["Context"]
    B --> C["Service"]
    C --> D["Repository"]

    D --> E["Database"]
    D --> F["gRPC"]
    D --> G["HTTP"]

    H["cancel / deadline"] --> B

    B -. cancellation .-> C
    C -. cancellation .-> D
    D -. cancellation .-> E
    D -. cancellation .-> F
    D -. cancellation .-> G

这就是:

Cancellation Propagation

12. Context 需要一直 for + select,会不会 CPU 空转?

不一定。

例如:

for {
    select {
    case <-ctx.Done():
        return

    default:
    }
}

这种确实容易形成:

busy loop

CPU 不断执行:

检查
↓
没取消
↓
继续
↓
检查

但是正确的阻塞式写法通常是:

select {
case <-ctx.Done():
    return

case task := <-taskChannel:
    handle(task)
}

没有:

default

时,goroutine 可以被 runtime 挂起等待。

所以:

select + 没有 default

更接近:

Event Wait

而不是:

CPU Polling

13. 有 Context 就一定要手写 select 吗?

也不是。

如果下游 API 本身支持 Context:

db.QueryContext(ctx, sql)

grpcClient.GetUser(ctx, request)

http.NewRequestWithContext(ctx, ...)

只需要:

继续把 ctx 往下传

即可。

你真正需要自己检查:

ctx.Done()

的一般是:

长时间 CPU 计算

func calculate(
    ctx context.Context,
) error {

    for i := 0; i < 1_000_000_000; i++ {

        if i%10000 == 0 {
            select {
            case <-ctx.Done():
                return ctx.Err()

            default:
            }
        }

        calculateSomething(i)
    }

    return nil
}

Channel / Worker

func worker(
    ctx context.Context,
    tasks <-chan Task,
) {

    for {
        select {

        case <-ctx.Done():
            return

        case task := <-tasks:
            handle(task)
        }
    }
}

14. Go Context 仍然不是强制 Kill

这是理解 Context 最重要的一点。

它属于:

Cooperative Cancellation

也就是:

协作式取消。

它不是:

kill goroutine

不是:

kill thread

更不是:

kill -9

例如:

func calculate(
    ctx context.Context,
) {

    for {
        heavyCalculate()
    }
}

即使:

cancel()

这个方法也不会自己停。

因为:

它根本没有检查 ctx

所以 Context 表达的是:

“这个任务已经没有继续执行的必要了,请尽快停止。”

而不是:

“操作系统,请立刻杀掉这个任务。”


15. Context Cancel 会不会关闭 TCP 长连接?

通常不会。

而且:

很多情况下也不应该。

例如 gRPC 基于 HTTP/2:

flowchart TD
    A["TCP Connection"]

    A --> B["Stream 1: RPC A"]
    A --> C["Stream 3: RPC B"]
    A --> D["Stream 5: RPC C"]
    A --> E["Stream 7: RPC D"]

如果:

RPC B

超时:

cancel()

更合理的是取消:

Stream 3

而不是:

整个 TCP Connection

否则:

RPC A
RPC C
RPC D

都会一起失败。

所以:

Cancel RPC
    ≠
关闭 TCP Connection
    ≠
停止远端业务

一定要把这三个概念分开。


16. Java 和 Go 真正的区别

Go:

flowchart TD
    A["context.Context"]

    A --> B["Deadline"]
    A --> C["Cancel"]
    A --> D["Done"]

    D --> E["HTTP"]
    D --> F["gRPC"]
    D --> G["Database"]
    D --> H["Business Code"]

整个生态有一个相对统一的:

Cancellation Signal

而传统 Java:

flowchart TD
    A["CompletableFuture"]

    A --> B["Future Timeout"]

    C["HTTP"] --> D["HTTP Timeout / Cancel"]

    E["Dubbo"] --> F["RPC Timeout"]

    G["Redis"] --> H["Command Timeout"]

    I["MySQL"] --> J["Query Timeout / Statement.cancel"]

它们之间并没有天然统一。

因此 Java 更像:

CompletableFuture
负责异步任务编排

各中间件 Client
负责自己的生命周期

17. CompletableFuture.allOf() 一个失败,需要取消其他任务吗?

答案:

看业务语义。

可以分成两种。


Fail Fast

假设:

A
B
C

三个任务必须全部成功。

任何一个失败:

整个任务没有意义

例如:

flowchart TD
    A["Task A"]
    B["Task B"]
    C["Task C"]

    A --> D["allOf"]
    B --> D
    C --> D

    A --> E["Failure"]
    E --> F["Cancel B"]
    E --> G["Cancel C"]

这种情况下:

一个失败
→ 其他任务应该尽快停止

Best Effort

BFF 首页通常不是这种模型。

例如:

Banner
帖子
活动
推荐

Banner 挂了:

帖子仍然有价值
活动仍然有价值

所以应该:

{
  "banner": [],
  "posts": [
    {}
  ],
  "activities": [
    {}
  ]
}

而不是:

Banner Failure
      ↓
Cancel Everything

所以:

是否取消 sibling task,首先是业务问题,然后才是技术问题。


18. 比 Timeout 更重要的是 Deadline

假设:

整个请求最大 500ms

如果每层都配置:

HTTP 500ms
Dubbo 500ms
Redis 500ms
MySQL 500ms

其实存在严重问题。

例如:

flowchart LR
    A["Controller<br/>100ms"] --> B["Dubbo<br/>500ms"]
    B --> C["MySQL<br/>500ms"]

理论上整个调用可能远远超过:

500ms

因为每一层:

重新开始计时

真正应该传播的是:

Deadline

例如:

总预算
500ms

Controller
花 100ms

剩余
400ms

RPC
花 150ms

剩余
250ms

DB
最多还能使用
250ms

流程:

flowchart LR
    A["Request<br/>500ms"]

    A --> B["Controller<br/>-100ms"]

    B --> C["Remaining<br/>400ms"]

    C --> D["RPC<br/>-150ms"]

    D --> E["Remaining<br/>250ms"]

    E --> F["Database<br/>max 250ms"]

这是:

Deadline Propagation

而不是:

Timeout Propagation

两者差别非常大。


19. Java 可以实现自己的 DeadlineContext

Java 虽然没有 Go 那样统一的 context.Context,但我们完全可以在基础框架层设计一个非常薄的:

public final class DeadlineContext {

    private final long deadlineNanos;

    private DeadlineContext(
            long deadlineNanos
    ) {
        this.deadlineNanos =
                deadlineNanos;
    }

    public static DeadlineContext timeout(
            Duration timeout
    ) {

        return new DeadlineContext(
                System.nanoTime()
                        + timeout.toNanos()
        );
    }

    public Duration remaining() {

        long remaining =
                deadlineNanos
                        - System.nanoTime();

        return Duration.ofNanos(
                Math.max(
                        remaining,
                        0
                )
        );
    }

    public boolean expired() {
        return remaining().isZero();
    }
}

请求开始:

DeadlineContext context =
        DeadlineContext.timeout(
                Duration.ofMillis(500)
        );

然后:

flowchart TD
    A["DeadlineContext<br/>500ms"]

    A --> B["Controller"]

    B --> C["Service"]

    C --> D["HTTP"]
    C --> E["Dubbo"]
    C --> F["Redis"]
    C --> G["MySQL"]

    D --> H["ctx.remaining()"]
    E --> H
    F --> H
    G --> H

所有底层客户端:

根据剩余时间
设置当前调用的 timeout

20. 但是 DeadlineContext 仍然不能直接取消底层资源

它只解决:

统一 Deadline 语义

真正的资源释放仍然应该由不同 Client 完成。

例如:

HTTP
↓
OkHttp Call.cancel()

Dubbo
↓
RPC timeout / cancellation

Redis
↓
Lettuce command timeout

MySQL
↓
Statement query timeout
Statement.cancel()

因此架构应该是:

flowchart TD
    A["DeadlineContext"]

    A --> B["DependencyInvoker"]

    B --> C["HTTP Adapter"]
    B --> D["Dubbo Adapter"]
    B --> E["Redis Adapter"]
    B --> F["MySQL Adapter"]

    C --> G["OkHttp"]
    D --> H["Dubbo"]
    E --> I["Lettuce"]
    F --> J["JDBC / Connector-J"]

    G --> K["Native Timeout / Cancel"]
    H --> K
    I --> K
    J --> K

Context:

负责统一语义

Client:

负责真正资源生命周期

21. Java 有没有现成的开源方案?

比较接近的方案是:

Resilience4j

它可以提供:

TimeLimiter
CircuitBreaker
Bulkhead
Retry
RateLimiter

例如:

TimeLimiterConfig config =
        TimeLimiterConfig.custom()
                .timeoutDuration(
                        Duration.ofMillis(500)
                )
                .cancelRunningFuture(true)
                .build();

但这里仍然必须注意:

cancelRunningFuture = true

并不意味着:

OkHttp Call.cancel()
JDBC Statement.cancel()
Dubbo Provider Stop
Redis Command Stop

全部自动发生。

所以 Resilience4j 更应该被定位成:

Call Governance

而不是:

IO Resource Cancellation Framework

22. 我更推荐的 Java 基础框架设计

如果让我设计公司级基础框架,我不会自己重新实现:

HTTP Client
Redis Client
JDBC
Dubbo
CircuitBreaker

这些基础组件。

我只会做一层:

Dependency Governance

最终架构:

flowchart TD
    A["Incoming Request"]

    A --> B["DeadlineContext"]

    B --> C["DependencyInvoker"]

    C --> D["Resilience4j"]
    C --> E["Micrometer"]
    C --> F["Tracing"]

    D --> G["HTTP Adapter"]
    D --> H["Dubbo Adapter"]
    D --> I["Redis Adapter"]
    D --> J["MySQL Adapter"]

    G --> K["OkHttp"]
    H --> L["Dubbo"]
    I --> M["Lettuce"]
    J --> N["HikariCP + JDBC"]

    K --> O["Native Timeout / Cancel"]
    L --> O
    M --> O
    N --> O

整体负责:

Deadline
Cancellation
Timeout
Circuit Breaker
Retry
Bulkhead
Metrics
Tracing
Error Classification

23. 推荐的中间件组合

场景推荐
HTTPOkHttp
RPCDubbo
RedisLettuce
MySQL Connection PoolHikariCP
JDBC DriverMySQL Connector/J
ORM / DAOMyBatis / JdbcTemplate
Circuit BreakerResilience4j
MetricsMicrometer
TraceOpenTelemetry

24. 基础框架应该重点统一什么?

我反而不建议:

所有数据库调用必须使用 MyDatabaseClient

所有 HTTP 必须使用 MyHttpClient

这种侵入非常大的设计。

更值得统一的是:

Deadline
Timeout
Metrics
Tracing
Retry
Circuit Breaker
Bulkhead
Error Classification

也就是说:

统一治理,而不是统一所有调用 API。


25. Metrics 应该统一

例如:

dependency.requests
dependency.duration
dependency.errors
dependency.timeout
dependency.cancelled

Tag:

type=http|dubbo|redis|mysql

service=user-service

operation=queryUser

result=success|timeout|cancel|error

还可以细分:

dependency.pool.wait
dependency.connect.duration
dependency.ttfb
dependency.read.duration
dependency.query.duration

最终 Grafana 可以看到:

flowchart LR
    A["Request"]

    A --> B["Pool Wait"]
    B --> C["DNS"]
    C --> D["Connect"]
    D --> E["TTFB"]
    E --> F["Response Read"]

    F --> G["Total Duration"]

这样真正发生问题时,我们才知道:

到底慢在哪里?

而不是只看到:

CompletableFuture Timeout

26. 最后重新理解 Cancel

经过这一轮分析,我认为:

Cancel

至少应该拆成五层。

flowchart TD
    A["1. 不再等待结果"]

    A --> B["2. 停止本地计算"]

    B --> C["3. 停止本地 IO"]

    C --> D["4. 停止远程调用"]

    D --> E["5. 停止远程业务执行"]

    A1["CompletableFuture.cancel"] --> A
    B1["Interrupt / Cooperative Cancellation"] --> B
    C1["Call.cancel / Socket Cancel"] --> C
    D1["HTTP / RPC Cancellation"] --> D
    E1["DB Kill Query / Server Context Cancel"] --> E

对应关系:

第一层:不再等待结果

future.cancel(true);

或者:

future.orTimeout(
        500,
        TimeUnit.MILLISECONDS
);

第二层:停止本地计算

Thread Interrupt
Cooperative Cancellation

第三层:停止本地 IO

call.cancel();

第四层:停止远程调用

HTTP Request Cancellation

RPC Stream Cancellation

第五层:停止远端真正执行

例如:

数据库停止 SQL

Provider 停止业务计算

下游继续传播 Deadline

这才是最难的一层。


27. Java 与 Go 的最终对比

Go

flowchart TD
    A["context.Context"]

    A --> B["Deadline"]
    A --> C["Cancellation"]
    A --> D["Done"]

    D --> E["HTTP"]
    D --> F["RPC"]
    D --> G["Database"]
    D --> H["Business Code"]

    E --> I["Cooperative Cancellation"]
    F --> I
    G --> I
    H --> I

核心思想:

一个 cancellation signal
贯穿调用链

Java

flowchart TD
    A["Application Deadline"]

    A --> B["CompletableFuture"]

    A --> C["HTTP"]
    A --> D["Dubbo"]
    A --> E["Redis"]
    A --> F["MySQL"]

    C --> G["OkHttp Timeout / Cancel"]
    D --> H["Dubbo Timeout"]
    E --> I["Lettuce Timeout"]
    F --> J["JDBC Timeout / Cancel"]

    B --> K["Async Composition"]

核心思想:

统一 Deadline

但是:

底层生命周期
由不同 Client 管理

28. 结语

这篇文章最开始只是一个很简单的问题:

CompletableFuture.allOf(
        a,
        b,
        c
);

如果:

其中一个 Future 超时

那么:

要不要取消另外两个?

但一路往下思考,就会发现真正的问题其实不是:

CompletableFuture 怎么 cancel?

而是:

当一个请求已经失去业务价值以后,整个调用链上的无效工作应该如何尽快停止?

这个调用链可能包含:

Future
Thread
Socket
HTTP
RPC
Redis
MySQL
Remote Service

一个真正完善的 Cancel 体系,需要同时考虑:

业务层
↓
这个结果是否还有价值

任务层
↓
是否继续等待

客户端层
↓
是否终止 IO

RPC 层
↓
是否取消远程调用

服务端
↓
是否响应 cancellation

数据库
↓
是否停止 Query

基础框架
↓
是否存在 Deadline

可观测性
↓
取消之后资源是否真的释放

所以:

优雅地 Cancel,从来都不只是调用一个 future.cancel(true)。

它本质上是:

对一次分布式请求完整生命周期的治理。

而如果我们真的要在 Java 基础框架中解决这个问题,我认为最合理的方向不是重新造一个 HTTP、Redis 或 RPC 框架。

而是构建一层很薄的:

Deadline
+
Cancellation
+
Resilience
+
Observability

让 OkHttp、Dubbo、Lettuce、JDBC 这些成熟组件继续负责它们擅长的事情。

最终形成:

flowchart LR
    A["Request"]

    A --> B["Deadline"]

    B --> C["Governance"]

    C --> D["HTTP"]
    C --> E["RPC"]
    C --> F["Redis"]
    C --> G["MySQL"]

    D --> H["Native Cancel"]
    E --> H
    F --> H
    G --> H

    H --> I["Resource Released"]

我认为,这才是 Java 中真正值得讨论的:

如何优雅地 Cancel。


Share this post:

Previous Post
从对象头、栈帧到锁与 JIT:我的 JVM 学习脉络
Next Post
从 Contributor 到 Committer:开源框架维护的完整工作流