从零手写连接池:Redisson ConnectionsHolder设计与简化实现

文章目录

    • 一、背景知识:连接池解决什么问题
      • [1.1 TCP 连接的开销](#1.1 TCP 连接的开销)
        • [1.1.1 TCP 状态机:内核中的 12 种状态](#1.1.1 TCP 状态机:内核中的 12 种状态)
        • [1.1.2 三次握手:内核到底做了什么](#1.1.2 三次握手:内核到底做了什么)
        • [1.1.3 四次挥手:为什么需要 TIME_WAIT](#1.1.3 四次挥手:为什么需要 TIME_WAIT)
        • [1.1.4 短连接的真实成本](#1.1.4 短连接的真实成本)
      • [1.2 池化模式的核心公式](#1.2 池化模式的核心公式)
    • [二、Redisson 连接池核心:ConnectionsHolder 源码解析](#二、Redisson 连接池核心:ConnectionsHolder 源码解析)
      • [2.1 必知必会:三大核心数据结构](#2.1 必知必会:三大核心数据结构)
      • [2.2 异步信号量:AsyncSemaphore 源码解析](#2.2 异步信号量:AsyncSemaphore 源码解析)
      • [2.3 获取连接:acquireConnection 到 connectTo 的完整链路](#2.3 获取连接:acquireConnection 到 connectTo 的完整链路)
      • [2.4 归还连接:releaseConnection 与 Entry 冻结](#2.4 归还连接:releaseConnection 与 Entry 冻结)
      • [2.5 初始化连接:initConnections 串行预创建](#2.5 初始化连接:initConnections 串行预创建)
    • 三、简化实现:MyConnectionPool
      • [3.1 核心数据结构映射](#3.1 核心数据结构映射)
      • [3.2 获取连接:同步版 acquireConnection](#3.2 获取连接:同步版 acquireConnection)
      • [3.3 归还连接:同步版 releaseConnection](#3.3 归还连接:同步版 releaseConnection)
      • [3.4 初始化连接:同步版 initConnections](#3.4 初始化连接:同步版 initConnections)
    • 全文小结

连接池是网络编程中不可或缺的基础设施。无论是数据库连接池(HikariCP、Druid)、Redis 客户端(Jedis、Lettuce、Redisson),还是 HTTP 连接池(Apache HttpClient),底层都依赖池化思想来管理 TCP 连接的生命周期。但大多数开发者只停留在「用池」层面,对连接池内部如何获取、复用、归还、限流缺乏系统认知。

但具体而言,连接池的核心数据结构是什么?如何用信号量控制并发连接数?空闲连接如何被复用,失败时又如何兜底创建新连接?Redisson 的 ConnectionsHolder 怎样用 AsyncSemaphore 实现全异步连接管理?如果去掉异步,用同步方式手写一个连接池,核心逻辑又是什么?

本文将先深入 Redisson 3.36.0 的 ConnectionsHolder 源码,逐层拆解 AsyncSemaphore 异步信号量、获取连接、归还连接、初始化连接的全链路,再自己手动简化实现 MyConnectionPool

一、背景知识:连接池解决什么问题

1.1 TCP 连接的开销

在聊连接池之前,我们需要先理解一个根本问题:为什么需要「池化」TCP 连接?要回答这个问题,必须深入 Linux 内核,看看一个 TCP 连接从建立到销毁,内核到底做了什么。

1.1.1 TCP 状态机:内核中的 12 种状态

Linux 6.8 内核在 include/net/tcp_states.h 中定义了 TCP 连接的 12 种状态(另有 1 种伪状态 TCP_BOUND_INACTIVE 用于 inet_diag 诊断):

c 复制代码
include/net/tcp_states.h

enum {
    TCP_ESTABLISHED = 1,  // 连接已建立,正常数据传输
    TCP_SYN_SENT,         // 客户端已发送 SYN,等待 SYN+ACK
    TCP_SYN_RECV,         // 服务端收到 SYN,已发送 SYN+ACK,等待 ACK
    TCP_FIN_WAIT1,        // 主动关闭方已发送 FIN,等待 ACK
    TCP_FIN_WAIT2,        // 主动关闭方收到 ACK,等待对端 FIN
    TCP_TIME_WAIT,        // 主动关闭方收到 FIN 并回复 ACK,等待 2MSL
    TCP_CLOSE,            // 连接已关闭
    TCP_CLOSE_WAIT,       // 被动关闭方收到 FIN 并回复 ACK,等待本地 close()
    TCP_LAST_ACK,         // 被动关闭方已发送 FIN,等待 ACK
    TCP_LISTEN,           // 服务端监听状态
    TCP_CLOSING,          // 双方同时关闭,已发送 FIN 并收到 FIN,等待 ACK
    TCP_NEW_SYN_RECV,     // 新 SYN_RECV(用于 SYN Cookie)
    TCP_BOUND_INACTIVE,   // 伪状态(用于 inet_diag 诊断)
    TCP_MAX_STATES        // 状态总数,始终在最后
};

这 12 种状态之间通过严格的状态机规则转换,每一次转换都涉及内核中 struct tcp_sock 对象的状态字段更新和相应的定时器管理。下面的 ASCII 图展示了完整的状态转换路径:

1.1.2 三次握手:内核到底做了什么

以 Linux 6.8 为例,客户端调用 connect() 后,内核经过以下调用链发送 SYN 包:

上面的调用链中,有几个关键细节值得注意:

初始序列号 ISNtcp_v4_connect() 调用 secure_tcp_seq() 生成一个基于时间戳和密钥的随机序列号,防止 TCP 序列号预测攻击。这是 Linux 内核的安全基石之一。

SYN 重传定时器tcp_connect() 在发送 SYN 后立即启动重传定时器 icsk_rto(Retransmission Timeout),默认初始值在 /proc/sys/net/ipv4/tcp_syn_retries 控制下,最多重传 6 次(指数退避:1s → 2s → 4s → 8s → 16s → 32s,总计约 63 秒)。

服务端收到 SYN 的处理 :当 SYN 包到达服务端,tcp_rcv_state_process()TCP_LISTEN 分支调用 icsk->icsk_af_ops->conn_request() 处理连接请求,分配 struct request_sock 并发送 SYN+ACK。

客户端收到 SYN+ACKtcp_rcv_synsent_state_process()tcp_input.c)校验 ACK 序列号(ACK 必须在 snd_unasnd_nxt 之间),校验时间戳(PAWS 防回绕),然后调用 tcp_finish_connect() 将状态转为 TCP_ESTABLISHED

从时间角度看,三次握手涉及 1.5 个 RTT(Round-Trip Time):

在跨机房网络延迟 50ms 的场景下,仅建连就至少需要 75ms(1.5 RTT),远超大多数业务逻辑处理时间。

1.1.3 四次挥手:为什么需要 TIME_WAIT

连接关闭的一方调用 close() 后,内核通过 tcp_close_state() 查表推进状态机。该函数在 tcp.c 中定义了一个状态转换表 new_state[]

c 复制代码
net/ipv4/tcp.c

// TCP 关闭状态转换表
static const unsigned char new_state[16] = {
  /* current state:        new state:      action: */
  [0 /* (Invalid) */] = TCP_CLOSE,
  [TCP_ESTABLISHED]   = TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  [TCP_SYN_SENT]      = TCP_CLOSE,
  [TCP_SYN_RECV]      = TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  [TCP_FIN_WAIT1]     = TCP_FIN_WAIT1,
  [TCP_FIN_WAIT2]     = TCP_FIN_WAIT2,
  [TCP_TIME_WAIT]     = TCP_CLOSE,
  [TCP_CLOSE]         = TCP_CLOSE,
  [TCP_CLOSE_WAIT]    = TCP_LAST_ACK  | TCP_ACTION_FIN,
  [TCP_LAST_ACK]      = TCP_LAST_ACK,
  [TCP_LISTEN]        = TCP_CLOSE,
  // 其余未列出的状态(TCP_CLOSING、TCP_NEW_SYN_RECV 等)同样转为 TCP_CLOSE
};

TCP_ACTION_FIN 标志位(值为 1 << TCP_CLOSE)指示 tcp_close_state() 返回后是否需要发送 FIN 包。内核源码中的 RED-PEN 注释对此做了说明:TCP_ESTABLISHED → TCP_FIN_WAIT1TCP_CLOSE_WAIT → TCP_LAST_ACK 的转换是合法的,但 FIN 包可能尚未发送(此时内核状态领先于 RFC 定义的状态),而 TCP_SYN_RECV → TCP_FIN_WAIT1 的转换在实际中是不可能的。

四次挥手的完整序列如下:

TIME_WAIT 状态 是四次挥手中最关键的阶段。Linux 内核中 TCP_TIMEWAIT_LEN 定义为 60 秒(即 2MSL,Maximum Segment Lifetime 默认为 30 秒)。TIME_WAIT 存在的意义有两个:

  1. 确保最后一个 ACK 能到达对端 :如果主动关闭方发送的最后一个 ACK 丢失,对端会在 LAST_ACK 状态重传 FIN,主动关闭方需要保持在 TIME_WAIT 以重发 ACK。
  2. 防止旧连接的数据包混入新连接:2MSL 的时间窗口确保网络中所有属于该连接的「迷途」数据包都已过期,不会干扰同一四元组的新连接。
1.1.4 短连接的真实成本

综合以上分析,每次请求都新建 TCP 连接的开销可以量化为三个维度:

时间成本:三次握手至少 1.5 RTT + 四次挥手至少 1.5 RTT + TIME_WAIT 60s。在跨机房 50ms RTT 场景下,仅建连和拆连就消耗 150ms,而业务逻辑可能只需 1ms。

内存成本 :每个 TCP 连接在内核中对应一个 struct tcp_sock(包含 struct inet_connection_sockstruct sock 等多个嵌套结构),以及至少一个 struct sk_buff 用于发送/接收缓冲区。默认情况下,Linux 的 tcp_rmem(接收缓冲区)默认值为 [4096, 131072, 6291456](最小/默认/最大,单位字节),tcp_wmem(发送缓冲区)默认值为 [4096, 16384, 4194304](最小/默认/最大,最大值随内存大小浮动)。此外,每个连接消耗一个文件描述符(FD),默认 RLIMIT_NOFILE 上限为 1024。

状态成本TIME_WAIT 状态的连接在 60 秒内无法释放,占用端口和 struct tcp_timewait_sock 内存。在短连接高并发场景下,客户端端口范围(net.ipv4.ip_local_port_range 默认 32768-60999,约 28000 个端口)可能被 TIME_WAIT 连接耗尽,导致 connect() 返回 EADDRNOTAVAIL

连接池的本质,就是用空间换时间 :预先创建一批连接并保持 ESTABLISHED 状态,请求到来时直接复用,用完归还,彻底绕过三次握手、四次挥手和 TIME_WAIT 的开销。

1.2 池化模式的核心公式

所谓池化模式,是指将资源管理抽象为三个核心操作的模式,几乎所有连接池、线程池、对象池都遵循这一公式:

三要素:资源复用(队列缓冲)+ 并发控制(信号量)+ 生命周期管理(状态机)

  • 资源复用 :用一个队列存放空闲资源。获取时从队列头部取出,归还时放回队列尾部。队列为空时,触发创建新资源。

  • 并发控制 :用一个信号量(Semaphore)控制同时使用的资源数量。信号量 permits 等于池的最大容量,获取前先 acquire 许可,归还后 release 许可。

  • 生命周期管理 :连接池需要处理连接失效(isActive() 检查)、节点宕机(Entry 冻结)、优雅关闭(shutdown)等状态转换。

    用一句话概括:连接池 = 信号量限流 + 空闲队列复用 + 兜底创建 。下面我们将深入 Redisson 的 ConnectionsHolder 源码,看看这套公式在工业级项目中的具体实现。

二、Redisson 连接池核心:ConnectionsHolder 源码解析

ConnectionsHolder 是 Redisson 连接池的核心类 ,位于 org.redisson.connection 包下,负责管理到单个 Redis 节点的所有 TCP 连接。每个 Redis 节点(Master 或 Slave)对应一个 ClientConnectionsEntry,每个 Entry 内部包含一个 ConnectionsHolder 实例。

2.1 必知必会:三大核心数据结构

在深入方法调用链之前,我们先看 ConnectionsHolder 的三个核心字段,这是理解后续所有逻辑的基础:

java 复制代码
org.redisson.connection.ConnectionsHolder

// 持有所有已创建的连接(含使用中),用于 shutdown 统一关闭
private final Queue<T> allConnections = new ConcurrentLinkedQueue<>();

// 空闲连接双向队列,poll() 从头部取,addFirst() 归还到头部(活跃连接优先复用)
private final Deque<T> freeConnections = new ConcurrentLinkedDeque<>();

// 异步信号量,permits = poolMaxSize,控制同时活跃连接数不超上限
private final AsyncSemaphore freeConnectionsCounter;

这三个字段的协作关系如下:

  • allConnections全集,持有池中所有已创建的连接引用,无论该连接正在被使用还是空闲。它的唯一作用是 shutdown 时遍历关闭所有连接。

  • freeConnections子集 ,只存放当前空闲的连接。使用 ConcurrentLinkedDeque(双向队列)而非 ConcurrentLinkedQueue,是因为归还时需要将活跃连接放到头部(addFirst),不活跃连接放到尾部(addLast),实现优先级复用。

  • freeConnectionsCounter限流器 ,构造时初始化为 poolMaxSize,获取连接前必须 acquire 一个许可,归还后 release 一个许可。这套机制保证了同一时刻活跃连接数不超过 poolMaxSize

    AsyncSemaphore 是 Redisson 自研的异步信号量,这是整个连接池设计中最精妙的部分,我们单独分析。

2.2 异步信号量:AsyncSemaphore 源码解析

Redisson 的定位是全异步 Redis 客户端,所有网络 IO 操作都基于 Netty 的 EventLoop 线程完成。因此,java.util.concurrent.Semaphore 的同步阻塞模型不合适------它会让 EventLoop 线程阻塞等待,导致该线程上的其他 IO 事件无法处理。

AsyncSemaphore 的解决方案是:将「等待许可」建模为一个 CompletableFuture,放入等待队列,当许可可用时异步完成这个 Future

上面的时序图清晰地展示了 acquire() 的异步等待流程:先将 Future 放入等待队列,然后尝试调度,如果栈深度超过阈值则提交到 ExecutorService 执行,否则直接递归调用。最终当 release() 被调用时,tryRun() 从队列中取出一个等待者,完成其 Future。

来看核心源码:

java 复制代码
org.redisson.misc.AsyncSemaphore#acquire

// 返回 CompletableFuture,许可可用时异步完成
public CompletableFuture<Void> acquire() {
    CompletableFuture<Void> future = new CompletableFuture<>();
    listeners.add(future);                          // 将等待者加入队列
    // 若 Future 异常完成,从队列中移除,避免后续 tryRun 唤醒已取消的等待者
    future.whenComplete((r, e) -> {
        if (e != null) {
            listeners.remove(future);
        }
    });
    tryForkAndRun();                                 // 尝试调度
    return future;
}
java 复制代码
org.redisson.misc.AsyncSemaphore#tryForkAndRun

// 栈溢出保护:当调用栈深度超过 25 * val 时,提交到线程池异步执行
private void tryForkAndRun() {
    if (executorService != null) {
        int val = tasksLatch.get();
        if (stackSize.get() > 25 * val
                && tasksLatch.compareAndSet(val, val + 1)) {
            executorService.submit(() -> {
                tasksLatch.decrementAndGet();
                tryRun();
            });
            return;
        }
    }
    tryRun();                                        // 否则直接递归调用
}

tryForkAndRun 的设计非常巧妙。当 release() 被频繁调用时,tryRun() 可能形成深度递归,极端情况下会导致 StackOverflowError。Redisson 的解决方案是:用一个 stackSize 计数器追踪当前递归深度,超过阈值(25 * val)时,将 tryRun() 提交到 ExecutorService 执行,从而打断递归链。

java 复制代码
org.redisson.misc.AsyncSemaphore#tryRun

// 核心调度逻辑:counter 递减后尝试完成一个等待者
private void tryRun() {
    while (true) {
        if (counter.decrementAndGet() >= 0) {        // ① 尝试获取许可
            CompletableFuture<Void> future = listeners.poll();
            if (future == null) {                    // ② 有许可但无等待者
                counter.incrementAndGet();            // 归还许可
                if (listeners.isEmpty()) {
                    return;                          // 队列空,退出
                }
                continue;                            // 继续尝试
            }

            boolean complete;
            if (executorService != null) {
                stackSize.incrementAndGet();
                complete = future.complete(null);    // ③ 唤醒等待者
                stackSize.decrementAndGet();
            } else {
                complete = future.complete(null);
            }
            if (complete) {
                return;                              // 成功唤醒一个等待者
            } else {
                counter.incrementAndGet();            // ④ 死等待者回收
                continue;
            }
        }

        if (counter.incrementAndGet() <= 0) {
            return;                                  // ⑤ 无可用许可,等待 release
        }
    }
}
java 复制代码
org.redisson.misc.AsyncSemaphore#release

// 释放许可:counter 加 1,触发 tryForkAndRun 唤醒等待者
public void release() {
    counter.incrementAndGet();
    tryForkAndRun();
}

tryRun() 中有两个容易忽略的关键细节:

死等待者回收(第④步)future.complete(null) 返回 false 时,说明这个 Future 已经被取消(例如调用方超时后取消了等待)。此时 counter 加回,continue 跳过,防止已取消的 Future 占用 permits。如果这里不做回收,被取消的等待者会永久占用一个 permit,导致连接池容量逐渐缩减。

许可未用完处理(第②步):counter 减到可用的值,但等待队列为空,说明此时没有调用方在等待。counter 加回后检查队列是否为空------如果不为空(说明在检查期间有新等待者加入),继续循环尝试。

2.3 获取连接:acquireConnection 到 connectTo 的完整链路

掌握了 AsyncSemaphore 之后,获取连接的逻辑就非常清晰了。核心入口是 acquireConnection(RedisCommand),它分为三步:信号量许可获取 → 空闲队列轮询 → 兜底创建新连接

上面的时序图展示了获取连接的完整流程:先获取信号量许可,然后从空闲队列轮询,命中则直接返回,未命中则通过 connectionCallback 创建新连接。无论哪种路径,最终都通过 connectedSuccessful 完成 promise。

java 复制代码
org.redisson.connection.ConnectionsHolder#acquireConnection

// 获取连接的对外入口:先获取信号量许可,再 connectTo
public CompletableFuture<T> acquireConnection(RedisCommand<?> command) {
    CompletableFuture<T> result = new CompletableFuture<>();

    CompletableFuture<Void> f = acquireConnection(); // ① 异步获取信号量许可
    f.thenAccept(r -> {
        connectTo(result, command);                   // ② 许可拿到后 connectTo
    });
    result.whenComplete((r, e) -> {
        if (e != null) {
            f.completeExceptionally(e);               // ③ 失败时传播异常
        }
    });
    return result;
}
java 复制代码
org.redisson.connection.ConnectionsHolder#connectTo

// 先 poll 空闲连接,没有再 createConnection
private void connectTo(CompletableFuture<T> promise, RedisCommand<?> command) {
    if (promise.isDone()) {                           // 已被取消
        releaseConnection();                          // 归还信号量许可
        return;
    }

    T conn = pollConnection(command);                 // ① 从空闲队列取
    if (conn != null) {
        connectedSuccessful(promise, conn);            // ② 取到了,直接返回
        return;
    }

    createConnection(promise);                        // ③ 没取到,创建新连接
}

connectTo 的逻辑非常简洁:先尝试复用,再兜底创建。promise.isDone() 的检查是必要的------从 acquireConnection() 返回的 CompletableFuture 可能在等待许可期间被调用方取消,此时需要立即归还许可。

java 复制代码
org.redisson.connection.ConnectionsHolder#pollConnection

// 遍历空闲队列,找到第一个活跃连接
private T pollConnection(RedisCommand<?> command) {
    int size = freeConnections.size();
    for (int i = 0; i < size; i++) {
        T conn = freeConnections.poll();              // 从头部取出
        if (conn == null) {
            return null;
        }
        if (conn.isActive()) {                        // 连接活跃,直接返回
            if (i > 0) {
                log.debug("skipped connections with inactive channel: {}", i);
            }
            if (changeUsage) {
                conn.incUsage();                      // 可选:引用计数递增
            }
            return conn;
        }
        freeConnections.addLast(conn);                // 不活跃,放到队尾
    }
    return null;
}

pollConnection 的核心设计是循环检查 :遍历 freeConnections,遇到不活跃连接时 addLast() 放回队尾。这样不活跃的连接不会阻塞队列,后续请求仍然可以取到队首的活跃连接。如果所有连接都不活跃,返回 null,触发 createConnection

java 复制代码
org.redisson.connection.ConnectionsHolder#createConnection

// 通过 connectionCallback 创建新连接
private void createConnection(CompletableFuture<T> promise) {
    CompletionStage<T> connFuture = connectionCallback.apply(client);
    connFuture.whenComplete((conn, e) -> {
        if (e != null) {
            releaseConnection();                      // 失败 → 释放信号量许可
            promise.completeExceptionally(e);
            return;
        }

        log.debug("new connection created: {}", conn);
        allConnections.add(conn);                     // 成功 → 加入 allConnections

        if (changeUsage) {
            promise.thenApply(c -> c.incUsage());
        }
        connectedSuccessful(promise, conn);            // 完成 promise
    });
}
java 复制代码
org.redisson.connection.ConnectionsHolder#connectedSuccessful

// 完成 promise,若已被取消则归还连接
private void connectedSuccessful(CompletableFuture<T> promise, T conn) {
    if (!promise.complete(conn)) {                    // 返回 false 说明已被取消
        releaseConnection(conn);                      // 归还连接到空闲队列
        releaseConnection();                          // 释放信号量许可
    }
}

connectedSuccessful 中有一个关键的双重检查promise.complete(conn) 返回 false 时,说明 promise 已经被取消(例如调用方超时后取消了 CompletableFuture)。此时必须立即归还连接并释放信号量许可,否则连接会泄漏------既不在空闲队列中,也不被任何调用方持有。

2.4 归还连接:releaseConnection 与 Entry 冻结

归还连接的主入口是 releaseConnection(ClientConnectionsEntry, T),它处理了两种场景:正常归还和节点宕机后的冻结归还。

上面的时序图展示了归还连接的两个分支:冻结状态下直接关闭连接,正常状态下放回空闲队列。归还后统一释放信号量许可,唤醒等待队列中的下一个调用方。

java 复制代码
org.redisson.connection.ConnectionsHolder#releaseConnection

// 归还连接的主入口:处理冻结 + 正常归还
public void releaseConnection(ClientConnectionsEntry entry, T connection) {
    if (entry.isFreezed()) {                           // ① 节点宕机,Entry 已冻结
        connection.closeAsync();                       // 直接关闭连接
        getAllConnections().remove(connection);
    } else {
        releaseConnection(connection);                 // ② 正常归还
    }
    releaseConnection();                               // ③ 释放信号量许可
}

Entry 冻结机制 是 Redisson 连接池的一个重要设计。当 Redis 节点宕机时,ClientConnectionsEntry 会被标记为 freezed 状态。此后,所有归还到该 Entry 的连接不再放回空闲队列,而是直接关闭。这避免了「脏连接」被后续请求复用------一个已经断开但尚未被检测到的连接,如果被放回队列,下一个调用方拿到后会立即失败。

java 复制代码
org.redisson.connection.ConnectionsHolder#releaseConnection

// 将连接放回空闲队列,活跃连接 addFirst 优先复用
private void releaseConnection(T connection) {
    if (connection.isClosed()) {                       // 连接已关闭,直接返回
        return;
    }

    if (client != null && client != connection.getRedisClient()) {
        connection.closeAsync();                       // client 不匹配,关闭连接
        return;
    }

    connection.setLastUsageTime(System.nanoTime());    // 记录最后使用时间
    if (connection.isActive()) {
        freeConnections.addFirst(connection);          // 活跃连接 → 队首(优先复用)
    } else {
        freeConnections.addLast(connection);           // 不活跃 → 队尾
    }
    if (changeUsage) {
        connection.decUsage();                         // 可选:引用计数递减
    }
}

归还时 addFirstaddLast 的区分体现了优先复用 策略:活跃连接放到队首,下次 pollConnection 时优先被取出。不活跃的连接放到队尾,避免被立即复用。同时,client 匹配检查防止了连接被错误归还到不属于它的 ConnectionsHolder

2.5 初始化连接:initConnections 串行预创建

连接池初始化时,需要预创建 minimumIdleSize 个连接放入空闲队列。Redisson 采用串行创建而非并行,原因有二:一是避免启动时对 Redis 服务端造成瞬时连接压力,二是串行创建失败时可以精确定位到第几个连接失败。

上面的时序图展示了串行初始化的流程:每个连接依次执行「获取许可 → 创建连接 → 放入空闲队列 → 释放许可」的循环。初始化完成后,空闲队列中有 minIdleSize 个连接,信号量 permits 全部空闲。

java 复制代码
org.redisson.connection.ConnectionsHolder#initConnections

// 串行预创建 minIdleSize 个连接
public CompletableFuture<Void> initConnections(int minimumIdleSize) {
    if (minimumIdleSize == 0) {
        return CompletableFuture.completedFuture(null);
    }

    CompletableFuture<Void> f = createConnection(minimumIdleSize, 1);
    for (int i = 2; i <= minimumIdleSize; i++) {
        int k = i;
        f = f.thenCompose(r -> createConnection(minimumIdleSize, k)); // 串行链式调用
    }
    return f.thenAccept(r -> {
        log.info("{} connections initialized for {}", minimumIdleSize, client.getAddr());
    });
}
java 复制代码
org.redisson.connection.ConnectionsHolder#createConnection

// 创建第 index 个连接(用于初始化阶段)
private CompletableFuture<Void> createConnection(int minimumIdleSize, int index) {
    CompletableFuture<Void> f = acquireConnection();       // ① 获取信号量许可
    return f.thenCompose(r -> {
        CompletableFuture<T> promise = new CompletableFuture<>();
        createConnection(promise);                         // ② 创建 TCP 连接
        return promise.handle((conn, e) -> {
            if (e == null) {
                if (changeUsage) {
                    conn.decUsage();
                }
                addConnection(conn);                       // ③ 放入空闲队列
                releaseConnection();                       // ④ 释放信号量许可
            }

            if (e != null) {
                // 初始化失败,清理所有已创建连接
                for (RedisConnection connection : getAllConnections()) {
                    if (!connection.isClosed()) {
                        connection.closeAsync();
                    }
                }
                getAllConnections().clear();

                int total = index - 1;
                String errorMsg;
                if (total == 0) {
                    errorMsg = "Unable to connect to Redis server: " + client.getAddr();
                } else {
                    errorMsg = "Unable to init enough connections amount! Only "
                            + total + " of " + minimumIdleSize
                            + " were initialized. Redis server: " + client.getAddr();
                }
                Exception cause = new RedisConnectionException(errorMsg, e);
                throw new CompletionException(cause);
            }
            return null;
        });
    });
}

初始化阶段有两个值得注意的细节:

信号量许可的「借---还」循环 :每次创建连接前 acquireConnection()(获取一个 permit),创建成功后 releaseConnection()(归还 permit)。初始化完成后,信号量 permits 恢复到 poolMaxSize,空闲队列中有 minIdleSize 个连接。这保证了初始化过程不会永久占用 permits。

失败时的错误信息区分index == 1 时失败(第一个连接就失败),错误信息是 "Unable to connect to Redis server",说明 Redis 服务端可能不可达。index > 1 时失败,错误信息是 "Only X of Y were initialized",说明部分连接创建失败,可能是网络波动或服务端连接数限制。

三、简化实现:MyConnectionPool

在理解了 Redisson 的 ConnectionsHolder 全链路之后,我们来自己动手简化实现 MyConnectionPool。核心思路是:AsyncSemaphore 降维为 JUC 的 Semaphore,将异步回调改为同步阻塞,但保持核心数据结构和方法签名不变

3.1 核心数据结构映射

java 复制代码
// 所有连接(含活跃 + 空闲),对应 Redisson allConnections
private final Queue<Channel> allConnections = new ConcurrentLinkedQueue<>();

// 空闲连接队列,对应 Redisson freeConnections(简化为单向队列)
private final Queue<Channel> freeConnections = new ConcurrentLinkedQueue<>();

// 信号量,对应 Redisson AsyncSemaphore(降维为 JUC 同步信号量)
private final Semaphore freeConnectionsCounter;

与 Redisson 的字段一一对应,差异点如下:

字段 Redisson MyConnectionPool 差异
空闲队列 ConcurrentLinkedDeque(双向) ConcurrentLinkedQueue(单向) 放弃 addFirst/addLast 优先级策略
信号量 AsyncSemaphore(异步非阻塞) Semaphore(同步阻塞) 异步 → 同步降维

3.2 获取连接:同步版 acquireConnection

同步版的 acquireConnection 与 Redisson 的核心逻辑完全一致------先获取信号量许可,再从空闲队列取,最后兜底创建。唯一的区别是信号量等待方式:Semaphore.tryAcquire(timeout) 会阻塞当前线程,直到超时或获取到许可。

上面的时序图与 Redisson 的获取链路完全对应,唯一的区别在于信号量获取是同步阻塞的。

java 复制代码
// 获取连接入口:同步版,对应 Redisson ConnectionsHolder.acquireConnection(RedisCommand)
public CompletableFuture<Channel> acquireConnection() {
    if (closed) {
        CompletableFuture<Channel> f = new CompletableFuture<>();
        f.completeExceptionally(new IllegalStateException("Connection pool is closed"));
        return f;
    }

    CompletableFuture<Channel> result = new CompletableFuture<>();

    // ① 获取信号量许可(同步阻塞,对应 Redisson AsyncSemaphore.acquire())
    if (!tryAcquirePermit()) {
        result.completeExceptionally(
                new TimeoutException("Connection pool exhausted, timeout=" + connectTimeout + "ms"));
        return result;
    }

    // ② 许可拿到后 connectTo
    connectTo(result);

    // ③ 失败时归还信号量许可
    result.whenComplete((r, e) -> {
        if (e != null) {
            releaseConnection();
        }
    });

    return result;
}
java 复制代码
// 对应 Redisson ConnectionsHolder.connectTo
private void connectTo(CompletableFuture<Channel> promise) {
    if (promise.isDone()) {
        releaseConnection();
        return;
    }

    Channel conn = pollConnection();               // ① 从空闲队列取
    if (conn != null) {
        connectedSuccessful(promise, conn);         // ② 取到了,直接返回
        return;
    }

    createConnection(promise);                     // ③ 没取到,创建新连接
}
java 复制代码
// 对应 Redisson ConnectionsHolder.createConnection(CompletableFuture)
// 差异:Redisson 通过 connectionCallback 函数注入建连逻辑,本实现内联 Bootstrap.connect()
private void createConnection(CompletableFuture<Channel> promise) {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                }
            });

    ChannelFuture future = bootstrap.connect(address);
    future.addListener((ChannelFuture f) -> {
        if (f.isSuccess()) {
            Channel channel = f.channel();
            allConnections.add(channel);             // 成功 → 加入 allConnections
            connectedSuccessful(promise, channel);
        } else {
            releaseConnection();                     // 失败 → 释放信号量许可
            promise.completeExceptionally(f.cause());
        }
    });
}

与 Redisson 最大的差异在于 createConnection 的实现:Redisson 通过 connectionCallback 函数注入建连逻辑,实现了连接创建与连接池的解耦。简化版直接将 Bootstrap.connect() 内联,减少了抽象层级,但牺牲了灵活性。

3.3 归还连接:同步版 releaseConnection

java 复制代码
// 对应 Redisson ConnectionsHolder.releaseConnection(ClientConnectionsEntry, T)
// 差异:去掉 Entry 冻结判断,简化为两步:空闲队列归还 + 信号量释放
public void releaseConnection(Channel connection) {
    if (connection == null) {
        return;
    }

    if (!connection.isActive()) {
        // 连接已失效,从 allConnections 移除,释放信号量许可
        allConnections.remove(connection);
        releaseConnection();
        return;
    }

    // ① 归还到空闲队列
    addToFreeConnections(connection);
    // ② 释放信号量许可
    releaseConnection();
}

简化版去掉了 Entry 冻结机制(因为项目中不涉及 Redis 主从切换),归还逻辑精简为两步:将活跃连接放入空闲队列,释放信号量许可。连接失效时从 allConnections 中移除,确保不会在 shutdown 时重复关闭。

3.4 初始化连接:同步版 initConnections

java 复制代码
// 对应 Redisson ConnectionsHolder.initConnections
// 完全一致的串行创建逻辑,差异在于 thenCompose → handle() 同步回调
public CompletableFuture<Void> initConnections() {
    if (minIdleSize == 0) {
        return CompletableFuture.completedFuture(null);
    }

    CompletableFuture<Void> f = createConnection(1);
    for (int i = 2; i <= minIdleSize; i++) {
        int index = i;
        f = f.thenCompose(r -> createConnection(index));
    }
    return f.thenAccept(r -> {
        System.out.println("[MyConnectionPool] " + minIdleSize + " 个连接初始化完成");
    });
}
java 复制代码
// 对应 Redisson ConnectionsHolder.createConnection(minIdle, index)
// 四步:获取许可 → 创建连接 → 放入空闲队列 → 释放许可
private CompletableFuture<Void> createConnection(int index) {
    // ① 获取信号量许可
    if (!tryAcquirePermit()) {
        return CompletableFuture.failedFuture(
                new TimeoutException("initConnections: 获取信号量许可超时, index=" + index));
    }

    CompletableFuture<Channel> promise = new CompletableFuture<>();
    // ② 创建 TCP 连接
    createConnection(promise);

    return promise.handle((conn, e) -> {
        if (e == null) {
            // ③ 放入空闲队列
            addConnection(conn);
        }
        // ④ 释放信号量许可
        releaseConnection();

        if (e != null) {
            // 初始化失败,关闭已创建的连接
            for (Channel connection : allConnections) {
                if (connection.isActive()) {
                    connection.close();
                }
            }
            allConnections.clear();
            throw new CompletionException(
                    "initConnections: 第 " + index + " 个连接创建失败", e);
        }
        return null;
    });
}

初始化逻辑与 Redisson 完全一致:串行创建、失败时清理所有已创建连接、抛出明确的错误信息。核心差异仅在于 thenCompose 中的回调是同步执行的,不需要 AsyncSemaphore 的异步调度。

全文小结

本文聚焦连接池的核心设计,从 Redisson ConnectionsHolder 源码解析和 MyConnectionPool 简化实现两个维度,深入分析了连接池的获取、复用、归还、初始化全链路。

AsyncSemaphore 方面,它以 CompletableFuture 等待队列实现异步非阻塞的信号量,其栈溢出保护(tryForkAndRun 的阈值检测)和死等待者回收(complete 返回 false 时 counter 加回)体现了工程级严谨。在获取连接方面,acquireConnection 的三步走------信号量许可 → 空闲轮询 → 兜底创建,是连接池的通用获取范式,pollConnection 的循环检查确保只返回活跃连接,connectedSuccessful 的双重检查防止资源泄漏。在 MyConnectionPool 方面,将全异步模型降维为同步版本后,核心逻辑与 Redisson 一一对应,验证了「信号量限流 + 队列缓冲」这一池化公式的通用性。


原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。

相关推荐
学编程就要猛1 小时前
解析博客系统后端实现
java·mysql·jwt·摘要算法·加盐
笨蛋不要掉眼泪1 小时前
RabbitMQ消息队列:交换机机制
java·分布式·rabbitmq
米码收割机2 小时前
【项目】spring boot+vue3 宠物领养系统(源码+文档)【独一无二】
java·spring boot·宠物
名为沙丁鱼的猫7293 小时前
【审计日志组件实践复盘】前端工程化 + 后端DDD架构(AOP切面拦截日志) + MBG
前端·架构
刹那芳华19923 小时前
STMF+ESP-S+MQTT协议连接华为云端(附踩坑记录)
java·struts·华为
就改了8 小时前
Java8 日期处理(详细版)
java·python·算法
麻瓜生活睁不开眼9 小时前
Android16定制SearchLauncher新增投屏Cast桌面快捷图标完整实现
java·launcher·aosp16
IT小盘10 小时前
12-Prompt不等于一句话-System-User-Context三层结构
java·windows·prompt
都叫我大帅哥11 小时前
Spring Data JPA 查询之道:方法命名与示例查询完全指南
java·后端·spring