手写 Java 连接池:实践心得

在日常开发中,数据库连接的频繁创建与销毁往往成为性能瓶颈。为了理解并掌握底层缓存技术与设计模式的应用,我手写了一个简陋版的 Java 连接池 SanguiCP,并将代码开源在 GitHub 上。本文将梳理我的学习过程、核心实现思路以及关键代码片段,帮助你快速理解连接池原理与装饰器模式的实践。


一、为何需要连接池?

在 Java 应用中,通过 DriverManager.getConnection(url, user, password) 建立新连接时,JVM 与数据库进程需完成 TCP 连接、身份认证等一系列耗时操作。使用调试模式,你会发现在执行该行代码时,程序会出现明显卡顿。若请求并发量较高,无节制地新建连接,不仅浪费资源,还可能导致数据库服务压力过大甚至宕机。

面临的两大问题:

  1. 性能低下:每次新建连接都要完成网络握手与认证。

  2. 资源受限:连接数不受控,超过数据库承载上限时,会触发连接拒绝或服务崩溃。

为此,连接池(Connection Pool)成为现代后端开发的标配:

  • 预创建 & 缓存:应用启动时,提前构建一定数量的连接实例,存放于内存集合中。用户请求时,直接获取复用,无需重复创建。

  • 限量管理:通过最大连接数限制并发连接,超出时可阻塞等待或抛出超时异常,避免数据库超载。


二、缓存策略与设计模式

缓存策略(Cache)

核心思想:提前创建、存放内存、按需取用。常见应用包括:

  • Java 字符串常量池、整型常量池

  • 线程池

  • Redis、Ehcache 等第三方缓存组件

通过缓存机制,可以大幅减少对象构建开销,提高程序整体性能。

装饰器模式(Decorator)

SanguiCP 实现中,我对 java.sql.Connection 做了包装:

  • 被装饰者Connection 原始实现来自 DriverManager

  • 装饰对象SanguiConnection 实现了 Connection 接口,并持有底层连接实例及连接池引用。

  • 关键改造:重写 close() 方法,当用户关闭连接时,实际将装饰对象归还到池中,而非真正关闭底层连接;其他方法均委托给原始连接执行。

此方式无需修改原有 Connection 类,即可在运行时动态增强功能,符合开闭原则。


三、项目结构与配置

├── pom.xml
├── src
│   ├── main
│   │   ├── java/com/sangui/sanguicp
│   │   │   ├── SanguiDataSource.java   // 数据源核心实现
│   │   │   └── SanguiConnection.java   // 装饰器连接实现
│   │   └── resources
│   │       └── sanguicp.properties     // 配置示例
│   └── test
│       └── java/com/sangui/sanguicp/test
│           └── MyDataSourceTest.java   // 单元测试示例
└── README.md

配置示例(sanguicp.properties

# 驱动类名
driver=com.mysql.cj.jdbc.Driver
# 数据库连接URL
url=jdbc:mysql://localhost:3306/mybatis
user=root
password=333
# 连接池最大连接数
maxTotal=3
# 单次等待间隔(毫秒)
waitTime=1000
# 最大累计等待时长(毫秒)
maxWaitTime=10000

四、关键代码解读

1. SanguiDataSource.java

public class SanguiDataSource implements DataSource {
   private final LinkedList<Connection> pool = new LinkedList<>();

   public SanguiDataSource() {
       // 读取配置,预创建 maxTotal 个连接
       for (int i = 0; i < maxTotal; i++) {
           Connection raw = DriverManager.getConnection(url, user, password);
           pool.add(new SanguiConnection(raw, pool));
      }
  }

   @Override
   public Connection getConnection() throws SQLException {
       if (!pool.isEmpty()) {
           return pool.removeFirst();
      }
       // 池空则等待或超时抛出
       Thread.sleep(waitTime);
       return getConnection();
  }

   // 其他 DataSource 接口方法留空或返回默认,实现可拓展
}

要点:

  • 使用 LinkedList 支撑队列操作,支持高效的头部增删。

  • 递归调用 getConnection() 实现重复等待逻辑。

  • 超过 maxWaitTime 则抛出 SQLException,提醒使用者释放或检查连接。

2. SanguiConnection.java

public class SanguiConnection implements Connection {
   private final Connection delegate;
   private final LinkedList<Connection> pool;

   public SanguiConnection(Connection delegate, LinkedList<Connection> pool) {
       this.delegate = delegate;
       this.pool = pool;
  }

   @Override
   public void close() throws SQLException {
       // 回收到池中,而非关闭底层连接
       pool.add(this);
  }

   // 其他方法均委托给 delegate
   @Override public Statement createStatement() throws SQLException { return delegate.createStatement(); }
   // ...
}

要点:

  • 装饰器模式的经典实现:在 close() 中插入自定义逻辑,其他方法不做修改。

  • 连接被归还后,可再次通过 getConnection() 获取,达到复用效果。


五、使用示例

@Test
public void testSanguiCP() throws Exception {
   SanguiDataSource ds = new SanguiDataSource();

   // 连续获取三个连接
   Connection c1 = ds.getConnection();
   Connection c2 = ds.getConnection();
   Connection c3 = ds.getConnection();
   System.out.println(c1 + ", " + c2 + ", " + c3);

   // 超出 maxTotal 将进入等待
   // Connection c4 = ds.getConnection();

   // 归还连接
   c3.close();
   Connection c4 = ds.getConnection();
   System.out.println(c4); // 应与 c3 实例相同
}

六、总结与后续

通过此项目,我深入理解了:

  1. 数据库连接的性能成本:连接创建与认证的代价为何如此高。

  2. 缓存技术思维:提前构建、内存复用、动静分离的基本策略。

  3. 装饰器设计模式:在不侵入原有类的前提下,通过包装增强功能。

在后续迭代中,可考虑:

  • 支持连接失效检测与自动重连

  • 增加监控统计(活跃连接数、等待时长分布等)

  • 提供多种回收策略(FIFO、LIFO、超时释放)

欢迎访问并关注我的 GitHub 仓库:

https://github.com/WuSangui571/sanguicp
git@github.com:WuSangui571/sanguicp.git
  • 微信
  • 赶快加我聊天吧
  • QQ
  • 赶快加我聊天吧
  • weinxin
三桂

发表评论 取消回复 您未登录,登录后才能评论,前往登录