
文章目录
-
- 一、背景知识:连接池解决什么问题
-
- [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 包:

上面的调用链中,有几个关键细节值得注意:
初始序列号 ISN :tcp_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+ACK :tcp_rcv_synsent_state_process()(tcp_input.c)校验 ACK 序列号(ACK 必须在 snd_una 和 snd_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_WAIT1 和 TCP_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 存在的意义有两个:
- 确保最后一个 ACK 能到达对端 :如果主动关闭方发送的最后一个 ACK 丢失,对端会在
LAST_ACK状态重传 FIN,主动关闭方需要保持在 TIME_WAIT 以重发 ACK。 - 防止旧连接的数据包混入新连接: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_sock、struct 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(); // 可选:引用计数递减
}
}
归还时 addFirst 与 addLast 的区分体现了优先复用 策略:活跃连接放到队首,下次 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 一一对应,验证了「信号量限流 + 队列缓冲」这一池化公式的通用性。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。