HikariCP 源码里的设计模式,比连接池本身更值得学

HikariCP 源码里的设计模式,比连接池本身更值得学

有次做性能调优,把 Druid 换成 HikariCP,QPS 涨了 15%。运维同事问我是不是改了什么大逻辑,我说就换了个连接池。他不信。

结果我自己也不信,于是翻了一圈源码。翻完就一个感觉:这哪是连接池,这是一本设计模式教科书。HikariCP 的代码量不到 Druid 的十分之一,但设计模式的运用密度高得离谱。这篇文章拆解一下 HikariCP 里我发现的 4 种设计模式,每种都直接关联到它的性能优势。

代理模式:Connection 不是你拿到的那个 Connection

你从 HikariCP 拿到的 Connection,从来不是原生的 JDBC Connection。

HikariCP 生成了两个代理类:ProxyConnectionHikariProxyConnection。前者是手写的,负责接管 close() 方法的语义------你调用 connection.close(),它不会真的关闭物理连接,而是把连接还回池里。后者是 Javassist 动态生成的,代理了 Statement、PreparedStatement、CallableStatement 的所有方法,目的是拦截这些方法的调用并记录统计信息。

java 复制代码
// ProxyConnection.java 核心逻辑
public final void close() throws SQLException {
    // 不是真的 close,而是归还到连接池
    this.delegate = null; // 切断对原生连接的引用
    this.poolEntry.recycle(); // 归还
}

这个设计有几个好处:

一是 close() 的语义被悄悄替换了。业务代码里到处都是 try-with-resources,写完就 close,从来不用关心连接是怎么回收的。这正是代理模式的核心价值------在不改变接口的前提下,改变行为。

二是代理层是一个天然的拦截点。HikariCP 可以在这里记录 SQL 执行时间、连接的借用时长,而这些统计代码跟业务逻辑完全不耦合。你写你的 CRUD,它统计它的指标,两边谁也不认识谁。

三是 Javassist 生成的代理比 JDK 动态代理快。HikariCP 的作者 Brett Wooldridge 在 GitHub 上专门解释过:JDK 动态代理每次调用都要走反射,而 Javassist 生成的字节码跟手写代码几乎一样,调用的开销可以忽略不计。这一点的设计取舍非常务实------代理模式有很多种实现方式,但选哪个直接影响性能。

单例模式:连接池本身当然是单例

这个看起来没什么好说的,但 HikariCP 的单例做得比大部分人想的细致。

大部分人的单例就是 private static final HikariDataSource dataSource = new HikariDataSource(),但这其实不是 HikariCP 内部在用的方式。HikariCP 内部通过 HikariConfig 的不可变性来保证配置级别的单例语义------HikariConfig 一旦传给 HikariDataSource 的构造器,就会被深拷贝一份,后续修改 HikariConfig 不会影响已经创建的数据源。

java 复制代码
public HikariDataSource(HikariConfig configuration) {
    // 校验配置
    configuration.validate();
    // 深拷贝,切断引用
    this.config = new HikariConfig(configuration);
    // 这里才真正初始化连接池
    this.pool = new HikariPool(this);
}

这么做的意义在于:你不会因为 "不小心改了一下配置对象" 而把生产环境的连接池搞崩。HikariCP 用拷贝构造的方式强制了不可变性,这比写文档提醒「请不要在运行时修改配置」靠谱得多。

另外,HikariPool 这个核心类也不是你想建就能建的,它只由 HikariDataSource 创建,外部完全不可见。这个设计跟单例模式里「控制实例的创建入口」是一样的思路,只不过限制的不是实例数量,而是创建路径。

工厂模式:连接不是 new 出来的

HikariDataSource.getConnection() 的调用链相当长,但每一步的职责都很清楚:

scss 复制代码
HikariDataSource.getConnection()
  → HikariPool.getConnection()
    → ConcurrentBag.borrow()
      → 从空闲队列取或创建新连接
        → PoolEntry.createProxyConnection()
          → ProxyConnection 构造

PoolEntry 是整个链路里最核心的工厂角色。它封装了一个物理连接的所有状态------创建时间、最后使用时间、当前是否被借用------然后通过 createProxyConnection() 方法产出 ProxyConnection

java 复制代码
Connection createProxyConnection() {
    return new ProxyConnection(this, connection, ...);
}

这看上去就是一个 new,没什么设计模式可言,但关键是创建的条件判断。ConcurrentBag.borrow() 的逻辑是这样的:

  1. 先从当前线程的本地队列取(ThreadLocal 缓存)
  2. 取不到就从全局空闲队列取
  3. 再取不到就检查是否还能创建新连接(没到 maximumPoolSize
  4. 如果都不能,就等着别人归还(waitsynchronized 块上)

这个"先取后造"的逻辑让连接创建变成了一种懒加载策略------不用提前建一堆连接等着,用了再说。这也是 HikariCP 比 Druid 启动更快的原因之一。

状态模式:连接的一生

HikariCP 里每个连接有 5 种状态,定义在 PoolEntry 的常量里:

复制代码
NOT_IN_USE → IN_USE → RESERVED → REMOVED
              ↓
          MARKED_FOR_REMOVAL

这些状态的转换不是用 if-else 堆出来的,而是通过 PoolEntry 的 CAS 操作驱动的:

java 复制代码
// 借用连接
boolean evict = poolEntry.compareAndSet(STATE.NOT_IN_USE, STATE.IN_USE);

// 归还连接
poolEntry.setState(STATE.NOT_IN_USE);

// 标记驱逐
poolEntry.compareAndSet(STATE.IN_USE, STATE.MARKED_FOR_REMOVAL);

每个状态对应一套行为规则:

  • NOT_IN_USE:连接空闲,可以被借用
  • IN_USE:连接正在被业务线程使用
  • RESERVED:连接被保留了,用于连接泄漏检测
  • MARKED_FOR_REMOVAL:连接被标记为要移除,等待当前使用结束后清理
  • REMOVED:连接已从池中移除

为什么不用 if-else?因为连接的状态变化是并发的------有线程在借连接,有线程在还连接,有线程在扫超时连接做驱逐。用 CAS 保证状态的原子切换,比加锁快得多,也比 if-else 看着清晰。

总结几句

翻了 HikariCP 源码后我最大的感受是:设计模式不是用来装饰代码的花边,它是解决具体性能问题的工具。代理模式替它省了反射开销,工厂模式替它省了启动时间,状态模式替它省了锁竞争。每个模式都在直接服务于 HikariCP 的核心竞争力------快。

Brett Wooldridge 写这个连接池的时候,肯定不会先在脑子里过一遍 GoF 23 种模式再开始写。他只是写了正确的东西,而正确的东西碰巧符合这些模式的结构。

这就回到了设计模式学习的本质:不是背模式、套模式,而是理解了模式之后,你写出来的代码天然就带着模式的味道。


我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画加答题的方式讲,HikariCP 这种源码里的设计模式我也会陆续加进去。如果你觉得这种"从源码学模式"的思路有意思,搜一下「爪爪代码冒险记」或者等我后面的文章。

相关推荐
止语Lab2 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活2 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困2 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn2 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530142 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908402 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金2 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒3 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境3 小时前
提升 Python 代码健壮性的方法大盘点
后端·python