记一次使用GenericObjectPool的体验,避免自己重复造轮子

概述

相信大家在日常的开发中,或多或少都接触过对象池连接池等池化的内容,可能还自己手动实现或维护过这种 “池子”。

我也曾干过这种事情。当时要连接一个第三方平台进行接口调用,为了减少耗时和提升稳定性,手动实现了一个连接池。

当时对接完成后,刚开始效果并不理想。从请求到收到响应,整个过程耗时超过了 1 秒,且 RT (响应时间) 有抖动,有时会超过 1.5 秒。

排查后发现,建立连接这个过程的耗时超过了 200 毫秒,并且会有抖动。造成这个现象的原因是,建立连接的过程比较复杂,链路建立成功后,会升级协议、服务端客户端校验、交换秘钥、....。

为了提升效率,避免连接过程的耗时和耗时抖动,将连接过程提前,实现了一个连接池。

这个连接池定期检测连接情况,维持连接数量,可复用其维持的连接对象。

最终将请求的耗时下降到了600毫秒左右,减少了 RT 抖动。

但是,自己手动实现的 “池子” 造价高昂,而且可能还会产生 BUG,会发生生产事故。排查问题的过程,也会很让人头疼。

那有没有什么好的办法,可以帮我我们解决这种问题呢?让程序不至于太复杂,又可以有很高的健壮性?

答案,就是本文章的主题 GenericObjectPool

什么是 GenericObjectPool?

GenericObjectPool,是 Apache Commons Pool 库中的一个类,它提供了一个通用的对象池的实现,主要用于复用昂贵或稀缺资源,通过维护一组已创建的对象实例来减少创建和销毁对象的开销,从而提高系统性能

GenericObjectPool 允许用户配置和管理一个对象池,包括设置池的最大和最小对象数量、空闲对象过期时间、borrow(获取)和return(归还)对象时的行为策略等。这个类是高度可配置和灵活的,可以适用于多种不同类型的对象和使用场景。

GenericObjectPool 的特点:

  1. 对象回收与复用:当对象不再使用时,可以归还到池中,供后续请求重复使用,避免频繁创建和销毁对象的开销。
  2. 线程安全:内部实现支持多线程环境下的安全操作,允许多个线程同时从池中借用和归还对象。
  3. 可配置的参数:如最大对象数(maxTotal)、最大空闲对象数(maxIdle)、最小空闲对象数(minIdle)、超时设置等,可以根据具体需求调整池的行为。
  4. 监听器支持:允许添加事件监听器来监控对象池的状态变化,比如对象借用、归还、移除等事件。
  5. 异常处理策略:定义了对池满、池空等情况的处理逻辑,可以通过自定义或配置已有策略来应对这些情况。

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 也感知不到对象不可用,造成了系统性能不稳定。

所以,对于类似应用场景的同学,要注意 检测周期 的设置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值