概述
相信大家在日常的开发中,或多或少都接触过对象池、连接池等池化的内容,可能还自己手动实现或维护过这种 “池子”。
我也曾干过这种事情。当时要连接一个第三方平台进行接口调用,为了减少耗时和提升稳定性,手动实现了一个连接池。
当时对接完成后,刚开始效果并不理想。从请求到收到响应,整个过程耗时超过了 1 秒,且 RT (响应时间) 有抖动,有时会超过 1.5 秒。
排查后发现,建立连接这个过程的耗时超过了 200 毫秒,并且会有抖动。造成这个现象的原因是,建立连接的过程比较复杂,链路建立成功后,会升级协议、服务端客户端校验、交换秘钥、....。
为了提升效率,避免连接过程的耗时和耗时抖动,将连接过程提前,实现了一个连接池。
这个连接池定期检测连接情况,维持连接数量,可复用其维持的连接对象。
最终将请求的耗时下降到了600毫秒左右,减少了 RT 抖动。
但是,自己手动实现的 “池子” 造价高昂,而且可能还会产生 BUG,会发生生产事故。排查问题的过程,也会很让人头疼。
那有没有什么好的办法,可以帮我我们解决这种问题呢?让程序不至于太复杂,又可以有很高的健壮性?
答案,就是本文章的主题 GenericObjectPool。
什么是 GenericObjectPool?
GenericObjectPool,是 Apache Commons Pool 库中的一个类,它提供了一个通用的对象池的实现,主要用于复用昂贵或稀缺资源,通过维护一组已创建的对象实例来减少创建和销毁对象的开销,从而提高系统性能。
GenericObjectPool 允许用户配置和管理一个对象池,包括设置池的最大和最小对象数量、空闲对象过期时间、borrow(获取)和return(归还)对象时的行为策略等。这个类是高度可配置和灵活的,可以适用于多种不同类型的对象和使用场景。
GenericObjectPool 的特点:
- 对象回收与复用:当对象不再使用时,可以归还到池中,供后续请求重复使用,避免频繁创建和销毁对象的开销。
- 线程安全:内部实现支持多线程环境下的安全操作,允许多个线程同时从池中借用和归还对象。
- 可配置的参数:如最大对象数(maxTotal)、最大空闲对象数(maxIdle)、最小空闲对象数(minIdle)、超时设置等,可以根据具体需求调整池的行为。
- 监听器支持:允许添加事件监听器来监控对象池的状态变化,比如对象借用、归还、移除等事件。
- 异常处理策略:定义了对池满、池空等情况的处理逻辑,可以通过自定义或配置已有策略来应对这些情况。
GenericObjectPool maven 依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
GenericObjectPool 使用举例
可使用单例模式,来使用连接池。具体情况具体分析。
@Slf4j
public class MyClientPool implements Closeable {
private static final MyClientPool INSTANCE = new MyClientPool();
public static MyClientPool getInstance() {
return INSTANCE;
}
private final GenericObjectPool<MyClient> internalPool;
public MyClientPool() {
// 设置池的一些参数
GenericObjectPoolConfig<MyClient> poolConfig = new GenericObjectPoolConfig<>();
// 设置最大连接数量
poolConfig.setMaxTotal(10);
// 设置最大空闲数量
poolConfig.setMaxIdle(8);
// 设置最小空闲数量
poolConfig.setMinIdle(5);
// 向调用者输出“链接”资源时,是否检测是有有效,如果无效则从连接池中移除,并尝试获取继续获取。默认为false
poolConfig.setTestOnBorrow(true);
// 当连接池资源耗尽时,调用者最大阻塞的时间,超时将跑出异常。单位,毫秒数;默认为-1.表示永不超时.
poolConfig.setMaxWait(Duration.ofSeconds(55));
// “空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1.
poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30));
// 创建一个池
this.internalPool = new GenericObjectPool<>(new MyPooledObjectFactory(), poolConfig);
}
// 获取一个连接对象
public MyClient borrowObject() {
try {
MyClient client = internalPool.borrowObject();
log.info("borrowObject, [idle={}, active={}]", internalPool.getNumIdle(), internalPool.getNumActive());
return client;
} catch (Exception e) {
log.error("borrowObject exception \n", e);
throw new SystemException(e);
}
}
// 返还连接对象,当使用完成后要调用。borrowObject和returnObject需成对出现
public void returnObject(MyClient client) {
try {
internalPool.returnObject(client);
log.info("returnObject, [idle={}, active={}]", internalPool.getNumIdle(), internalPool.getNumActive());
} catch (Exception e) {
log.error("returnObject exception \n", e);
throw new SystemException(e);
}
}
// 获取对象池此时状态,供监控平台调用
public Map<String, Long> poolStatus() {
Map<String, Long> map = new HashMap<>(8);
map.put("Total", (long) internalPool.getNumActive() + internalPool.getNumIdle());
map.put("Active", (long) internalPool.getNumActive());
map.put("Idle", (long) internalPool.getNumIdle());
return map;
}
// 关闭池
@Override
public void close() {
try {
internalPool.close();
} catch (Exception e) {
log.error("internalPool close exception", e);
}
}
// 池对象工厂,用于管理创建、验证、激活、钝化以及销毁对象的方法。
private static class MyPooledObjectFactory extends BasePooledObjectFactory<MyClient> {
// 创建一个资源
@Override
public MyClient create() throws Exception {
log.info("create");
MyClient client = new MyClient();
// 如果有需要提前初始化的操作,放在此处进行初始化
client.connect();
return client;
}
@Override
public PooledObject<MyClient> wrap(MyClient client) {
return new DefaultPooledObject<>(client);
}
// 销毁一个对象
@Override
public void destroyObject(PooledObject<MyClient> pooledClient) throws Exception {
log.info("destroyObject");
// 如果有一些释放动作,在此处进行
MyClient client = pooledClient.getObject();
client.close();
}
// 检测对象是否有效
@Override
public boolean validateObject(PooledObject<MyClient> pooledClient) {
MyClient client = pooledClient.getObject();
boolean result = client.isOpen();
log.info("validateObject : {}", result);
return result;
}
// 激活对象
@Override
public void activateObject(PooledObject<MyClient> pooledClient) {
log.info("activateObject");
}
// 钝化对象
@Override
public void passivateObject(PooledObject<MyClient> pooledClient) {
log.info("passivateObject");
}
}
}
额外说明 - 检测周期
另外要着重说明下,检测周期这个属性。
// “空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1.
poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30));
如果你应用于连接池这种场景,建议你还是设置检测周期。
检测周期的具体数值,可以根据连接断开情况具体设置,最好,可以在连接断开的时间范围内,检测 2~3 次,这样可以让 GenericObjectPool 及时检测到连接对象是否有效,以便重新创建对象,维护空闲对象数。
另外,设置检测周期还有一个好处,可以在连接池对象激活后,自动创建最小空闲数个对象,并且会自动检测并维护对象池。
例如,前面讲到的连接池场景,我希望应用启动后,就建立与第三方平台的连接,最大程度保证请求的低延时。如果不设置检测周期,那么在当实际请求第三方平台时,才会建立连接,也就是说前期请求延迟还是高,并且在空闲一段时间后,GenericObjectPool 也感知不到对象不可用,造成了系统性能不稳定。
所以,对于类似应用场景的同学,要注意 检测周期 的设置。