4. 常见空闲连接回收策略:LRU、FIFO、TTL、基于健康检查的回收

空闲连接回收策略的选择直接影响连接池的命中率、资源利用率以及尾延迟的稳定性。不同的策略在回收效率、实现复杂度、对突发流量的适应性上存在显著差异。本节将逐一剖析四种主流策略的核心机制、适用场景与潜在风险。

4.1 策略全景对比

下表从回收依据、空间复杂度、时间复杂度、尾延迟影响四个维度对四种策略进行横向对比:

策略 回收依据 空间复杂度 时间复杂度 尾延迟影响
LRU 最近使用时间 O(n) O(1) 访问 / O(n) 淘汰 低(保留热点连接)
FIFO 创建时间 O(n) O(1) 中(可能淘汰热点)
TTL 绝对存活时间 O(n) O(n) 扫描 高(批量过期导致抖动)
健康检查 连接可用性 O(n) O(n) 探测 极低(主动剔除坏连接)
选型提示: 在压测场景下,LRU 通常能提供最稳定的尾延迟表现,因为它天然保留了最近被使用的“热连接”,避免了冷连接被误淘汰导致的建连开销。FIFO 适合连接生命周期极短、无热点的场景。

4.2 LRU(最近最少使用)策略

LRU 的核心思想是:淘汰最长时间未被使用的连接。它假设“最近被使用的连接在近期内最有可能被再次使用”。实现上通常采用双向链表 + 哈希表的结构,保证每次访问的时间复杂度为 O(1)。

实现要点:
  • 每次连接被借出或归还时,将该节点移动到链表头部。
  • 当需要回收空闲连接时,从链表尾部移除节点。
  • 哈希表用于 O(1) 定位链表节点,避免遍历查找。
// 伪代码:LRU 回收逻辑
class LRUConnectionPool {
    Map<Connection, Node> cache;
    DoublyLinkedList list;
    int maxIdle;

    void returnConnection(Connection conn) {
        Node node = cache.get(conn);
        list.moveToHead(node);  // 移到头部,标记为最近使用
    }

    Connection evictIdle() {
        Node tail = list.removeTail();  // 移除最久未使用的连接
        cache.remove(tail.conn);
        return tail.conn;
    }
}
LRU 双向链表结构示意图 Head Conn #5 (最新) Conn #3 Conn #1 (最久) Tail 回收流程 1. 新连接或刚被使用的连接插入 Head 侧 2. 每次访问命中,节点移动到 Head 3. 回收时从 Tail 侧移除 Conn #1(最久未使用)

4.3 FIFO(先进先出)策略

FIFO 按照连接创建时间的先后顺序进行回收,最早创建的连接优先被淘汰。实现简单,只需一个队列即可完成,时间复杂度为 O(1)。

尾延迟风险: FIFO 不考虑连接的使用频率。如果某个热点连接恰好是最早创建的,它会被优先回收,导致后续请求需要重新建立连接,增加 P99 延迟。在压测波动场景下,这种“误杀”会放大尾延迟抖动。
// 伪代码:FIFO 回收逻辑
class FIFOConnectionPool {
    Queue<Connection> idleQueue;
    int maxIdle;

    void returnConnection(Connection conn) {
        idleQueue.offer(conn);  // 入队尾部
    }

    Connection evictIdle() {
        return idleQueue.poll();  // 从队头移除最早创建的连接
    }
}

4.4 TTL(生存时间)策略

TTL 策略为每个连接设置一个固定的最大存活时间(如 60 秒)。当连接的空闲时间超过 TTL 阈值时,连接被标记为过期并回收。这种策略通常配合定时扫描线程使用。

关键参数:
  • ttlDuration: 连接的最大空闲存活时间,单位毫秒。
  • scanInterval: 定时扫描的间隔,通常为 ttlDuration 的 1/10 到 1/5。
  • batchSize: 每次扫描最多回收的连接数,防止一次性回收过多导致抖动。
// 伪代码:TTL 定时扫描回收
class TTLConnectionPool {
    List<IdleConnection> idleList;
    long ttlMs = 60000;  // 60 秒 TTL

    void scheduledEviction() {
        long now = System.currentTimeMillis();
        Iterator<IdleConnection> it = idleList.iterator();
        while (it.hasNext()) {
            IdleConnection ic = it.next();
            if (now - ic.lastReturnTime > ttlMs) {
                ic.conn.close();
                it.remove();
            }
        }
    }
}
批量过期雪崩: 如果大量连接在同一时间点创建(如服务重启后),它们会在同一时间点过期,导致定时扫描时批量关闭连接,紧接着大量请求同时触发建连,造成 CPU 和网络带宽的瞬时尖峰,显著拉高尾延迟。解决方案是引入 TTL 抖动(Jitter),为每个连接的 TTL 增加 ±10% 的随机偏移。

4.5 基于健康检查的回收

健康检查策略不依赖时间维度,而是通过主动探测连接的实际可用性来决定是否回收。常见的探测方式包括发送心跳 SQL(如 SELECT 1)或 TCP 探活。

该策略通常作为其他回收策略的补充层,用于剔除那些虽然未超时但实际已断开的“僵尸连接”。

// 伪代码:健康检查回收
class HealthCheckConnectionPool {
    ScheduledExecutorService scheduler;
    int checkInterval = 5000;  // 5 秒检查一次

    void healthCheck() {
        for (Connection conn : idleList) {
            if (!isValid(conn)) {  // 发送 SELECT 1 探测
                conn.close();
                idleList.remove(conn);
            }
        }
    }

    boolean isValid(Connection conn) {
        try (Statement stmt = conn.createStatement()) {
            stmt.executeQuery("SELECT 1");
            return true;
        } catch (SQLException e) {
            return false;
        }
    }
}
健康检查回收流程 空闲连接池 Conn A (正常) Conn B (断开) 健康检查器 定时发送 SELECT 1 探测 数据库 响应探测 回收决策 Conn A 保留 | Conn B 关闭并从池中移除

4.6 策略组合建议

在实际生产环境中,单一策略往往无法满足所有场景。推荐采用分层组合策略:

  • 第一层(快速回收): LRU 或 FIFO,用于控制空闲连接总数不超过 maxIdle。
  • 第二层(超时兜底): TTL + 抖动,防止连接长时间空闲占用资源。
  • 第三层(质量保障): 健康检查,定期剔除已断开的僵尸连接,避免应用层拿到坏连接。
压测调优建议: 在压测预热阶段,将 TTL 设置为压测持续时间的 2~3 倍,避免压测过程中触发批量回收。压测结束后,再恢复为正常的短 TTL 以快速释放资源。