分布式微服务系统架构第118集:Future池管理容器-CompletableFuture

199 阅读10分钟

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc…

1024bat.cn/

阶段说明涉及数据结构
创建 bossGroup、workerGroup分别管理接入连接和后续 IO 读写NioEventLoopGroup(内部有多线程执行)
配置 serverBootstrap设置各种 Netty 参数,组装 ServerBootstrapServerBootstrap
注册 childHandlerxxServerChInit 用于初始化每条新的连接 ChannelPipelineChannelInitializer<SocketChannel>
调用 bind(port)把服务绑定到 9000 端口,监听 TCP 连接ChannelFuture
客户端连接到服务器bossGroup 接收连接,workerGroup 负责后续的数据通信NioServerSocketChannelNioSocketChannel
连接初始化xxServerChInit 中添加的 Handler 被执行ChannelPipeline(责任链模式)
读写数据处理自定义 Handler 处理接收到的数据自定义消息类(比如 JT808 报文、定制指令)
服务关闭关闭所有资源,释放端口shutdownGracefully

主要涉及的数据结构变化

变化点变化描述
ServerBootstrap -> ChannelFuture通过 bind 端口后,返回 ChannelFuture 表示异步绑定结果。
NioEventLoopGroup -> NioEventLoop创建的 boss/worker 组内部是多个 NioEventLoop(线程 + Selector)。
SocketChannel -> ChannelPipeline每次有连接建立时,初始化 ChannelPipeline,里面串联各种 Handler。
AdaptiveRecvByteBufAllocator动态分配读缓冲区大小(避免内存浪费或过小导致频繁扩容)。
@Component
public class xxServerChInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 这里可以添加编解码器、业务处理器等
        pipeline.addLast(new FrameDecoder());   // 粘包拆包处理器
        pipeline.addLast(new ProtocolDecoder()); // 协议解码
        pipeline.addLast(new BusinessHandler()); // 具体业务逻辑
    }
}

这里的 pipeline 就像一条责任链,负责把客户端发过来的数据一层层加工和处理

阶段说明涉及类/数据结构
握手建立连接Netty接收连接,调用 initChannel 方法SocketChannel
创建责任链(Pipeline)pipeline()返回一个空白链表结构DefaultChannelPipeline
加入 IdleStateHandler监测180秒内是否有读写,否则触发 IdleStateEventIdleStateHandler
加入解码器 xxxDecoder收到数据时,自动从 ByteBuf 解析成业务 POJOxxDecoder
加入编码器 xxEncoder发送数据时,把业务对象转成 ByteBufxxEncoder
加入业务处理器 xxxServerHandle处理 decode后的业务消息,执行业务逻辑xxServerHandle

主要涉及的数据结构变化

阶段变化描述
SocketChannel.pipeline()初始化一个新的责任链 DefaultChannelPipeline
addLast 操作往 Pipeline 中按顺序插入 Handler,形成链表
ByteBuf -> BusinessMessage通过 Decoder 把原始字节流反序列化成对象
BusinessMessage -> ByteBuf通过 Encoder 把业务对象编码成字节流
IdleStateEvent超时后通过 pipeline 触发一个 userEventTriggered

JVM 调优:

1. 垃圾回收器选择

  • 建议:使用 G1 GC
    -XX:+UseG1GC
  • 特点:适合高并发、低延迟场景,GC暂停时间可控。

2. 堆内存配置

  • 根据实际连接数量和消息量,合理设置堆大小:

    -Xms2g -Xmx2g
    

    (初始和最大一致,避免动态扩容带来的卡顿)

3. Netty内存管理优化

  • Netty 默认用 PooledByteBufAllocator,确保启用池化:

    bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    
  • 减少小对象频繁分配带来的GC压力。

4. 线程数配置

  • bossGroup 和 workerGroup 线程数可以根据 CPU 核心数调整:

    new NioEventLoopGroup(cpu核心数 * 2)
    
  • 例子:8核机器可以用 new NioEventLoopGroup(16)

5. 防止内存泄漏

  • Handler 一定要释放资源,比如 ByteBuf 的 release()。
  • 监控日志中是否有 Netty 报出的 LEAK: ByteBuf.release() 警告。
  • 保活(180s心跳超时检测)
  • 解码(字节 -> 业务对象)
  • 编码(业务对象 -> 字节)
  • 核心业务处理(执行业务逻辑)
阶段主要处理
设备连接channelActive:记录Channel
设备首次发消息channelRead0解析
后续收消息更新心跳、解析处理业务逻辑、回写结果
设备掉线channelInactive清除映射关系
空闲超时userEventTriggered主动关闭超时连接

基于 ReentrantLock + Condition

  • 支持异步线程之间的通知/等待。

  • 支持超时控制。

  • 手动 signal 触发返回结果。

  • 兼容 java.util.concurrent.Future 接口规范。

Condition适合这种低开销、少量同步的场景,比如一个Future小对象,不走重型同步器(像 CompletableFuture 里面是用 Unsafe)。

  • 存储一批等待异步完成的 CondFuture
  • 通过 id 来匹配具体的 Future。
  • 控制最大Future数量,防止内存爆掉。
  • 定期清理超时未完成的 Future。
  • 提供 setSignal(setSyncFuture)去异步唤醒 Future。

非常适合应用在 客户端请求异步等待场景,比如:

请求下发一个任务,客户端阻塞等待响应结果,超时自动清理。

  • ConcurrentHashMap 保证多线程安全。
  • futureCount 作为保护阈值,避免堆积太多 Future。
  • invalidTime 检查超时的 Future,主动释放资源。
  • 支持动态唤醒(setSyncFuture)指定 Future。
  • 保持了轻量,没引入复杂调度任务,适合高性能环境。
项目现状改进
线程安全基本ok加强 remove/signal 一致性
类型安全泛型丢失T 泛型
并发性能粗锁double-check 减少锁
日志频繁只打异常日志
清理机制嵌套抽出 cleanExpiredFutures

补充:这个容器典型应用场景

  • Netty长连接服务器(客户端发起请求,需要服务器应答)
  • Kafka异步投递任务回调
  • RPC调用 Future 同步等待返回
  • IoT设备异步响应
  • 微服务之间的异步请求-响应

举个小例子,比如 Netty 收到一条业务请求,存个 CondFuture,异步发出去,下次 channelRead 回来,再通过 id 取出 Future,signal 一下返回响应,非常常见!

场景说明举例
Netty长连接服务器请求-响应模式,需要在异步事件驱动下,临时保存请求上下文。客户端发指令(比如充值请求),服务器先缓存 Future,异步处理完后 signal 返回结果。
Kafka异步投递回调生产者发送消息后,不是立即确认,需要等待消费端处理回调。Kafka 生产发出去后,异步监听回调 ack,等拿到ack再 signal Future。
RPC调用同步等待返回微服务之间异步RPC调用,client端需要同步等待结果。A系统请求B系统处理数据,不阻塞主线程,通过 Future await 等待异步回包。
IoT设备异步响应大量设备与云端交互,请求和响应间隔时间不确定。IoT终端发上传指令,云端收到保存 Future,等设备异步回执 signal。
微服务异步API网关网关收到异步任务,需要同步返回响应,需要短暂缓存请求。API网关请求分发到后端异步处理,Future await直到有结果,返回给前端。
延迟消息确认某些系统需要发送消息后等待业务确认或补充处理。下单后,发“支付成功消息”,等用户支付异步回调,signal Future并继续业务。
链路追踪跨服务调用需要追踪一条链路的请求链,保持一个临时状态直到结束。一个调用链起点保存 Future,链路回调后 signal 记录埋点结束。

(Netty里的超典型用法)

比如:

  1. 客户端发送一条请求,带 reqId=12345

  2. 服务器 Netty channelRead 收到请求后:

    • CondFutureContainer.getFuture("12345")
    • 保存一个 CondFuture
    • 继续异步向业务层派发请求
  3. 业务处理需要异步,比如访问数据库、下游系统、或者Kafka投递

  4. 后来异步事件返回,比如数据库处理好了/下游响应回来了

    • 业务代码调用 CondFutureContainer.setSyncFuture("12345", 响应内容)
    • 唤醒 Future,Netty 把响应写回客户端
  5. 完成一次完整的异步请求-响应!

示意伪代码:

// 收到请求
public void handleClientRequest(ChannelHandlerContext ctx, Request req) {
    CondFuture<Response> future = container.getFuture(req.getId());
    asyncBizProcess(req); // 异步处理
    Response response = future.await(30, TimeUnit.SECONDS);
    ctx.writeAndFlush(response); // 结果回来再响应客户端
}

// 异步处理完成后
public void asyncBizCallback(String id, Response result) {
    container.setSyncFuture(id, result);
}

⚡ 另外你提到 Kafka异步投递场景

也可以,比如:

  • 生产者投递消息
  • 但 Kafka ack确认需要异步监听
  • 可以用 Future 容器 await
  • 监听器回调 signalFuture

Kafka producer发送场景:

// 发送消息
public Response sendKafkaMsg(String id, Message msg) {
    CondFuture<Boolean> future = container.getFuture(id);
    kafkaTemplate.send("topic", msg);
    Boolean success = future.await(10, TimeUnit.SECONDS);
    return success ? Response.success() : Response.fail();
}

// Kafka 监听ack
@KafkaListener(...)
public void onKafkaAck(ProducerRecord record) {
    String id = record.key();
    container.setSyncFuture(id, true);
}

【使用 CondFuture 异步请求-响应】

使用 CondFutureContainer 管理异步请求-响应的标准流程


1. 客户端发送请求

客户端(比如 Web、APP、IoT设备)发来一个请求,请求携带一个全局唯一的 ID(reqId)。

2. 服务端 Netty 收到请求,准备 Future

public void handleClientRequest(ChannelHandlerContext ctx, Request req) {
    // 2.1 从容器中根据请求ID拿到一个CondFuture(如果不存在就新建)
    CondFuture<Response> future = container.getFuture(req.getId());

    // 2.2 异步处理请求(比如下游数据库、Kafka、其他微服务)
    asyncBizProcess(req); 

    // 2.3 当前线程阻塞等待 future 被 signal,超时时间设定,比如30秒
    Response response = future.await(30, TimeUnit.SECONDS);

    // 2.4 Future被唤醒或者超时,发送响应回客户端
    ctx.writeAndFlush(response); 
}

3. 异步业务处理(比如发Kafka消息)

public void asyncBizProcess(Request req) {
    // 发送消息到 Kafka,或者其他异步系统
    sendKafkaMsg(req.getId(), buildKafkaMessage(req));
}

4. 异步发送 Kafka 消息,继续挂 Future 等待回调

public Response sendKafkaMsg(String id, Message msg) {
    // 再次确保有Future存在(可以视业务而定)
    CondFuture<Boolean> future = container.getFuture(id);

    // 异步发送 Kafka 消息
    kafkaTemplate.send("topic", msg);

    // 等待异步Kafka确认回调
    Boolean success = future.await(10, TimeUnit.SECONDS); // 10秒超时
    return success ? Response.success() : Response.fail();
}

5. Kafka异步回调,唤醒对应 Future

@KafkaListener(topics = "topic")
public void onKafkaAck(ProducerRecord record) {
    // 获取发送时设置的 ID
    String id = record.key();
    // 唤醒对应 Future,传递回调结果
    container.setSyncFuture(id, true);
}

6. 服务端继续处理(被 signal 唤醒)

  • await() 被 signal 成功唤醒
  • 取到回调的数据(比如 Response)
  • 通过 Netty ctx.writeAndFlush(response) 把最终响应写回客户端
  • 完成整个闭环

🔥【完整时序图概览】

客户端  ->  服务端Netty    ->  Future挂起
          服务端异步处理   ->  Kafka投递
          Kafka发送成功    ->  Kafka回调
          Kafka回调监听器 ->  signal Future
          Netty线程唤醒    ->  响应客户端

🌟【一整套关键点总结】

关键动作说明
container.getFuture(id)新建或获取对应的 Future 实例
future.await(timeout)阻塞等待异步结果,设置超时时间
asyncBizProcess(req)异步处理逻辑,不要阻塞主线程
container.setSyncFuture(id, result)异步回调后,唤醒对应 Future
ctx.writeAndFlush(response)收到回调后,将结果响应给客户端
超时处理如果超时,返回超时错误,保护服务线程不会死等
容器清理机制定期清理过期的 Future,防止内存泄漏

🛠️【注意点和最佳实践】

  • req.getId() 必须保证全局唯一,不要冲突!
  • future.await(timeout) 记得设置超时时间,防止永远挂死。
  • Kafka、MQ等异步系统,需要能正确拿到回调的 id
  • CondFutureContainer 要考虑过期数据清理(可以加定时器清理,保证健壮性)。
  • 容器最大数量(futureCount)可以根据业务并发量调整。
  • Signal的时候可以带业务处理结果对象(比如 Response 或者处理状态)。
  • 高性能
  • 可扩展
  • 异步超时可控
  • 防止内存泄露
  • 支持高并发请求 的 异步请求-响应框架了!

而且可以随意套用在 Netty长连接Kafka消息投递IoT云端响应RPC调用 等各种场景!

带超时保护和定时清理 Future的优化版 CondFutureContainer

支持超时保护 + 定时清理过期 Future + 自动降级防爆内存

📌 signal的作用

在你这个 CondFuture 里面,signal(T object) 方法的意思是:

✅ 把【异步处理好的结果】塞进去(设置到object字段),
✅ 然后【唤醒】之前在 await() 方法里因为等待而挂起的线程。

简单说:通知“兄弟,结果来了,醒醒,继续跑吧!”

📦 源码回顾一下

比如你的 CondFuture 类里,signal 是这样实现的:

public void signal(T object) {
    this.object = object;
    lock.lock();
    try {
        condition.signal(); // 唤醒一个等待的线程
    } finally {
        lock.unlock();
    }
}

流程:

  1. this.object = object; 👉 先把结果保存。
  2. condition.signal(); 👉 再用 Conditionawait()里卡住的线程唤醒。
  3. 被唤醒的线程,就能拿到结果,继续干活了。

🌟 举个小例子串起来理解

  1. Netty业务线程,收到客户端请求时:
CondFuture<Response> future = container.getFuture(req.getId());
Response resp = future.await(30, TimeUnit.SECONDS); // 这里阻塞等待

它在await,等某人唤醒它。

  1. 异步处理线程,比如 Kafka 返回 ACK:
container.setSyncFuture(req.getId(), response); 

内部调用了 future.signal(response),把结果塞进去并 唤醒

  1. 于是第1步里挂起的线程醒来,拿到resp,继续往客户端写回响应。

🧠 核心理解一句话

signal 就是把异步返回结果通知到同步等待的线程,打通整个流程的关键。

🔥 总结口诀

步骤动作
线程1await() 等待别人唤醒
线程2业务处理完,signal(结果) 唤醒
线程1醒了,拿到结果继续跑