Understanding Java Connection Pools: Commons Pool 2, Jedis, and HikariCP
This article explains the principles of object pooling in Java, introduces the Commons Pool 2 library, demonstrates its use with Redis client Jedis, compares performance with JMH benchmarks, and details the configuration and advantages of the high‑performance HikariCP database connection pool.
大家好,现在介绍一款非常强大,高效,并且号称“史上最快连接池”。由此可见他是有多受人喜欢,并且
在SpringBoot2.0之后,采用的默认数据库连接池就是Hikari 。
我们知道的连接池有C3P0,DBCP,它们都比较成熟稳定,但性能不是十分好。所以有了BoneCP这个连接池,它是一个高速、免费、开源的JAVA连接池,它的性能几乎是C3P0、DBCP的25倍,十分强悍。
在我们平常的编码中,通常会将一些对象保存起来,这主要考虑的是对象的创建成本。
比如像线程资源、数据库连接资源或者 TCP 连接等,这类对象的初始化通常要花费比较长的时间,如果频繁地申请和销毁,就会耗费大量的系统资源,造成不必要的性能损失。
并且这些对象都有一个显著的特征,就是通过轻量级的重置工作,可以循环、重复地使用。
这个时候,我们就可以使用一个虚拟的池子,将这些资源保存起来,当使用的时候,我们就从池子里快速获取一个即可。
在Java 中,池化技术应用非常广泛,常见的就有数据库连接池、线程池等,本文主讲连接池,线程池我们将在后续的博客中进行介绍。
公用池化包 Commons Pool 2
我们首先来看一下 Java 中公用的池化包 Commons Pool 2,来了解一下对象池的一般结构。
根据我们的业务需求,使用这套 API 能够很容易实现对象的池化管理。
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>GenericObjectPool 是对象池的核心类,通过传入一个对象池的配置和一个对象的工厂,即可快速创建对象池。
public GenericObjectPool(
final PooledObjectFactory<T> factory,
final GenericObjectPoolConfig<T> config)案例
Redis 的常用客户端 Jedis,就是使用 Commons Pool 管理连接池的,可以说是一个最佳实践。下图是 Jedis 使用工厂创建对象的主要代码块。
对象工厂类最主要的方法就是makeObject,它的返回值是 PooledObject 类型,可以将对象使用 new DefaultPooledObject<>(obj) 进行简单包装返回。
redis.clients.jedis.JedisFactory,使用工厂创建对象。
@Override
public PooledObject<Jedis> makeObject() throws Exception {
Jedis jedis = null;
try {
jedis = new Jedis(jedisSocketFactory, clientConfig);
//主要的耗时操作
jedis.connect();
//返回包装对象
return new DefaultPooledObject<>(jedis);
} catch (JedisException je) {
if (jedis != null) {
try { jedis.quit(); } catch (RuntimeException e) { logger.warn("Error while QUIT", e); }
try { jedis.close(); } catch (RuntimeException e) { logger.warn("Error while close", e); }
}
throw je;
}
}我们再来介绍一下对象的生成过程,如下图,对象在进行获取时,将首先尝试从对象池里拿出一个,如果对象池中没有空闲的对象,就使用工厂类提供的方法,生成一个新的。
public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
//此处省略若干行
while (p == null) {
create = false;
//首先尝试从池子中获取。
p = idleObjects.pollFirst();
// 池子里获取不到,才调用工厂内生成新实例
if (p == null) {
p = create();
if (p != null) { create = true; }
}
//此处省略若干行
}
//此处省略若干行
}那对象是存在什么地方的呢?这个存储的职责,就是由一个叫作 LinkedBlockingDeque 的结构来承担的,它是一个双向的队列。
接下来看一下 GenericObjectPoolConfig 的主要属性:
// GenericObjectPoolConfig本身的属性
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
// 其父类BaseObjectPoolConfig的属性
private boolean lifo = DEFAULT_LIFO;
private boolean fairness = DEFAULT_FAIRNESS;
private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
private long minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private long evictorShutdownTimeoutMillis = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
private long softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
private EvictionPolicy
evictionPolicy = null;
private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME;
private boolean testOnCreate = DEFAULT_TEST_ON_CREATE;
private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
private boolean testOnReturn = DEFAULT_TEST_ON_RETURN;
private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;参数很多,要想了解参数的意义,我们首先来看一下一个池化对象在整个池子中的生命周期。
对象池在进行初始化时,要指定三个主要的参数:
maxTotal 对象池中管理的对象上限
maxIdle 最大空闲数
minIdle 最小空闲数
其中 maxTotal 和业务线程有关,当业务线程想要获取对象时,会首先检测是否有空闲的对象。如果有,则返回一个;否则进入创建逻辑。如果池中个数已经达到了最大值,就会创建失败,返回空对象。
对象在获取的时候,有一个非常重要的参数——最大等待时间(maxWaitMillis),默认 -1 表示永不超时,直到有对象空闲。
如果对象创建非常缓慢或使用非常繁忙,业务线程会持续阻塞(blockWhenExhausted 默认为 true),导致正常服务也不能运行。
面试题
一般面试官会问:你会把超时参数设置成多大呢?我一般会把最大等待时间设置成接口可以忍受的最大延迟,例如 500~1000ms。
超时之后,会抛出 NoSuchElementException 异常,请求会快速失败,这种 Fail Fast 思想在互联网应用非常广泛。
带有 evcit 字样的参数主要处理对象逐出,池化对象在运行时也会占用系统资源,需要适时回收。
四个 test 参数(testOnCreate、testOnBorrow、testOnReturn、testWhileIdle)决定了在创建、获取、归还、空闲检测时是否进行有效性检测,默认 false,生产环境建议仅开启 testWhileIdle 并调节检测间隔。
JMH 测试
使用连接池和不使用连接池,它们之间的性能差距到底有多大呢?下面是一个简单的 JMH 测试例子,对 Redis 的 key 设置随机值。
@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
public class JedisPoolVSJedisBenchmark {
JedisPool pool = new JedisPool("localhost", 6379);
@Benchmark
public void testPool() {
Jedis jedis = pool.getResource();
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
@Benchmark
public void testJedis() {
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
//此处省略若干行
}测试结果显示,使用连接池的方式吞吐量约为未使用连接池的 5 倍。
数据库连接池 HikariCP
HikariCP 源于日语“光る”,寓意软件工作速度如光速,它是 SpringBoot 中默认的数据库连接池。
它在池化技术之上通过编码技巧进一步提升性能,例如使用 FastList 替代 ArrayList、Javassist 优化字节码、实现无锁的 ConcurrentBag 等。
HikariCP 官方不推荐设置 minimumIdle,默认与 maximumPoolSize 相同;在实际业务中,数据库连接数 20~50 个通常足够,需根据业务属性调整。
对于不同业务类型,可考虑拆分连接池以避免资源争抢,或使用多个连接池分别处理快速响应和后台慢任务。
结果缓存池
池(Pool)与缓存(Cache)在保存对象或数据方面有相似之处,都是将加工后的结果存放在高速区域,以提升后续访问速度。
这种技术可以称为结果缓存池(Result Cache Pool),是一种综合性的优化手段。
小结
本文从 Commons Pool 2 开始,介绍了对象池的实现细节、关键参数及其调优,并通过 JMH 基准测试展示了 Jedis 使用池化后约 5 倍的性能提升;随后分析了 HikariCP 的高速特性和配置要点,建议在实际项目中合理设置池大小、超时和检测参数,以获得最佳性能。
当遇到对象创建成本高、创建耗时长且可重置复用的场景时,考虑使用对象池化技术,并结合监控和参数调优,实现系统性能的显著提升。
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.