对象池(连接池):commons-pool2源码解析:GenericObjectPool的abandonedConfig属性、removeAbandoned方法

在解析borrowObject方法时,方法内部最开始的有一小段逻辑如下

AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
        (getNumIdle() < 2) &&
        (getNumActive() > getMaxTotal() - 3) ) {
    removeAbandoned(ac); 
}
  1. 首先获取当前对象池的内部属性abandonedConfig,他是一个AbandonedConfig类的实例,赋值给ac局部变量
  2. 接下来进行一个逻辑判断,一共4个条件
    • ac不为空
    • ac的removeAbandonedOnBorrow属性为true
    • 当前空闲对象的数量 < 2
    • 当前活跃对象的数量 >(对象池最大数量-3)
  3. 如果判断为真,把ac当做参数传入,则执行removeAbandoned方法。

后两个条件都为真的时候,空闲对象最多1个,可供新申请的对象资源最多2个:说明对象池中的资源使用趋于饱和了,资源快耗尽了。这个时候新的的请求过来,如果请求量较大,1、2个剩余资源被争抢后,剩下的请求就只能排队等待或者直接返回异常了。

那么此时如果配置了遗弃对象管理机制的话,就可以针对活跃对象,进行一些检测,来移除那些长时间运行未被回收的对象。

我们首先来看下遗弃对象的管理配置都有哪些。也就是AbandonedConfig类都定义了哪些属性。

AbandonedConfig类解析

AbandonedConfig就是一个简单的java bean,里面定义了几个private属性以及提供了对应的getter、setter方法。我们主要对属性来进行解析。

public class AbandonedConfig {

    /**
     * Whether or not borrowObject performs abandoned object removal.
     * 如果设置为true,就意味着,在borrowObject方法中(从对象池中申请对象的时候)就可以进行遗弃对象的相关清理逻辑。
     * (当然是否能触发清理,还受限于其他条件)
     */
    private boolean removeAbandonedOnBorrow = false;

    /**
     * Whether or not pool maintenance (evictor) performs abandoned object removal.
     * 对象池本省可以通过GenericObjectPoolConfig配置后台异步清理任务,但是后台清理任务的主要职责是关注空闲(状态为空闲)对象的检测和清理。
     * 如果removeAbandonedOnMaintenance设置为true,就意味着,在对象池的异步清理任务中,也可以进行遗弃(状态为活跃)对象的相关清理。
     */
    private boolean removeAbandonedOnMaintenance = false;
    
    /**
     * Timeout in seconds before an abandoned object can be removed.
     * 这个时间,默认300s。如果对象池的一个对象被占用了超过300s还没有被释放,就认为是被调用方遗弃了。
     */
    private int removeAbandonedTimeout = 300;


    /**
     * Determines whether or not to log stack traces for application code
     * which abandoned an object.
     * 如果处理发了遗弃对象的回收和清理,是否要打印该对象的调用堆栈。
     * 生产环境十分不建议设为true,会很消耗性能。
     */
    private boolean logAbandoned = false;

    /**
     * PrintWriter to use to log information on abandoned objects.
     * Use of default system encoding is deliberate.
     * 这是一个日志输出器,用来定义调用堆栈日志的输出行为(输出到控制台、文件等)。默认输出到控制台。
     * 如果logAbandoned为false,就不会有输出行为。
     */
    private PrintWriter logWriter = new PrintWriter(System.out);

    /**
     * If the pool implements {@link UsageTracking}, should the pool record a
     * stack trace every time a method is called on a pooled object and retain
     * the most recent stack trace to aid debugging of abandoned objects?
     * 这个UsageTracking从名字上来直观翻译,叫做使用追踪,指的就是池中对象的使用追踪。UsageTracking也是一个接口,GenericObjectPool实现了该接口。关于UsageTracking我们后续单独详细解析,
     * 什么叫使用?
     *  就是只要是调用了池中对象的任何方法,就算使用了池中对象。
     * 什么叫追踪?
     *  就是在池中某个对象的任何一个方法被调用时,都会创建一个调用堆栈快照。     
     *  logAbandoned为true时,useUsageTracking也为true时,那么回收被遗弃的对象时,就会打印该对象最后一次的调用堆栈信息了。
     *  如果useUsageTracking为true,即便是logAbandoned为false,那么每次对象的方法调用,一样还是会创建调用堆栈对象。只不过最终被回收时不会打印输出。
     *  生产环境该属性也不建议设置为true。
     */
    private boolean useUsageTracking = false;

}

通过以上的属性解析,我们就了解完了AbandonedConfig类的作用,接下来就可以来分析到底是如何处理这些被遗弃对象的了。

removeAbandoned方法解析

/**
* Recover abandoned objects which have been checked out but
* not used since longer than the removeAbandonedTimeout.
* 回收那些被取走的,但是超过了很长时间没有被使用的(被遗弃的)对象。
* 很长时间是多长的,取决于removeAbandonedTimeout配置了多少,默认300秒。
* @param ac The configuration to use to identify abandoned objects
*/
private void removeAbandoned(AbandonedConfig ac) {
    // Generate a list of abandoned objects to remove
    final long now = System.currentTimeMillis(); // 获取当前的毫秒时间戳
    /*
        ac.getRemoveAbandonedTimeout()获取的是AbandonedConfig的removeAbandonedTimeout属性值,默认300秒
        当前的毫秒时间戳 - (removeAbandonedTimeout->默认300秒 * 1000) = 300秒之前的毫秒时间戳
    */
    final long timeout =
            now - (ac.getRemoveAbandonedTimeout() * 1000L); 
    ArrayList<PooledObject<T>> remove = new ArrayList<PooledObject<T>>(); // 初始化一个list,用来存储需要删除的对象池中的废弃的对象
    Iterator<PooledObject<T>> it = allObjects.values().iterator(); // 对象池中所有对象的集合转换为一个迭代器
    while (it.hasNext()) { // 遍历所有对象
        PooledObject<T> pooledObject = it.next(); // 取出一个对象
        synchronized (pooledObject) { // 锁定该对象
            /*
                如果该对象的状态为ALLOCATED,也就是使用中状态(使用中不一定就是执行中,也不一定真的还在被使用)
                pooledObject.getLastUsedTime()返回的就是对象的lastUseTime属性值,见名知意,就是该对象上一次被使用的时间
                    这个值我们在解析borrowObject时提到过一点,就是在调用对象的allocte方法时,会把当前的毫秒时间戳赋值给lastUseTime
                所以如果:lastUseTime < timeout ,说明该对象已经在(removeAbandonedTimeout -> 默认300秒)的时间里没有被再次使用了。
            */
            if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
                    pooledObject.getLastUsedTime() <= timeout) {
                pooledObject.markAbandoned(); // 把该对象标记为ABANDONED(可舍弃)
                remove.add(pooledObject); // 把该对象添加到待移除列表中
            }
        }
    }

    // Now remove the abandoned objects
    Iterator<PooledObject<T>> itr = remove.iterator(); // 待移除列表转换为迭代器
    while (itr.hasNext()) { // 遍历所有待移除对象
        PooledObject<T> pooledObject = itr.next();
        if (ac.getLogAbandoned()) { // 如果logAbandoned配置为true
            pooledObject.printStackTrace(ac.getLogWriter()); // 打印调用栈日志
            /* 一般情况下,这个日志我们不关心,而且打印调用栈日志会消耗jvm性能,所以生产环境一般建议:logAbandoned不要设置为true */
        }
        try {
            invalidateObject(pooledObject.getObject()); // 调用invalidateObject方法,来作废(销毁、释放资源)该对象
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

小结

为什么会触发被遗弃对象移除逻辑
  • removeAbandonedOnBorrow配置为true。基于内部校验逻辑,检测到池资源即将耗尽。
  • removeAbandonedOnMaintenance配置为true。如果开启了后台异步回收机制,那么在每一个异步检测周期,也会顺带进行遗弃对象的检测和移除。
如何定义被遗弃对象

使用时间超过300秒,未归还的。
300秒可通过removeAbandonedTimeout进行配置。

如何查找被遗弃对象

从所有活跃对象中根据对象最后一次借出时间与当前时间比对。超过removeAbandonedTimeout设置的时间长度的。

如何处理被遗弃对象

调用invalidateObject方法来进行销毁。
根据logAbandoned配置来决定是否打印调用堆栈日志。

生产环境建议配置

logAbandoned、useUsageTracking都维持默认false设置。设置为true会对性能损耗较大。

  • 堆栈信息的创建是基于Exception对象的创建来实现的。Exception对象是一个很重的对象,会抓取线程栈快照信息。
  • 因为调用栈信息一般都比较庞大(想想我们的排查问题时的异常堆栈),这么多的信息通过日志打印输出,本身对IO消耗就会比较大。

AbandonedConfig如何使用

GenericObjectPool类提供了一个包含AbandonedConfig入参的构造方法以及一个public的setAbandonedConfig方法。

public GenericObjectPool(PooledObjectFactory<T> factory,
            GenericObjectPoolConfig config, AbandonedConfig abandonedConfig) {
    this(factory, config);
    setAbandonedConfig(abandonedConfig);
}
    
    
public void setAbandonedConfig(AbandonedConfig abandonedConfig) throws IllegalArgumentException {
    if (abandonedConfig == null) {
        this.abandonedConfig = null;
    } else {
        this.abandonedConfig = new AbandonedConfig();
        this.abandonedConfig.setLogAbandoned(abandonedConfig.getLogAbandoned());
        this.abandonedConfig.setLogWriter(abandonedConfig.getLogWriter());
        this.abandonedConfig.setRemoveAbandonedOnBorrow(abandonedConfig.getRemoveAbandonedOnBorrow());
        this.abandonedConfig.setRemoveAbandonedOnMaintenance(abandonedConfig.getRemoveAbandonedOnMaintenance());
        this.abandonedConfig.setRemoveAbandonedTimeout(abandonedConfig.getRemoveAbandonedTimeout());
        this.abandonedConfig.setUseUsageTracking(abandonedConfig.getUseUsageTracking());
    }
} 

对象池内部,并没有直接使用外部传进来的AbandonedConfig对象,而是自己内部重新new了一个新的,然后把所有入参对象的属性拷贝到新对象上。这样做是为了保证对象池的安全,如果直接使用传进来的配置对象引用,那么该配置对象在池外被更改属性的话,会直接影响到对象池的行为。
在基于spring框架进行业务开发层面,几乎不会自己去new一个GenericObjectPool,我们最常用的对象池是连接池,基于common-pool的比较常用的连接池

  • 数据库连接池:dbcp
  • redis连接池:jedis、lettuce
dbcp和AbandonedConfig有关的配置

dbcp是一个老牌的,曾经非常流行,一个很多项目中都被使用的数据库连接池。他的底层就依赖common-pool(dbcp2依赖common-pool2)。目前数据库连接池这一块有很多后起之秀,比较有名气的应该就属druid和hikari,在监控、扩展、性能层面相比于dbcp更有优势。当然这两个不是我们本文分析的重点。我们还是看下dbcp中如何使用AbandonedConfig,通过dbcp的官网可以很容易的查到dbcp的配置参数。
DBCP2的Abandoned配置
除了最后一个属性abandonedUsageTracking的命名和useUsageTracking有些差异之外,其他属性都是同名的。
参考dbcp2官网:http://commons.apache.org/proper/commons-dbcp/configuration.html

jedis和AbandonedConfig有关的配置

jedis是一个redis的java客户端框架,用来提供针对redis进行操作的java api。可能截止目前还是应用最广的redis java客户端。他同样也提供redis连接池的功能。官网上有如下一段简短的描述说明了,redis的连接池要怎么配置。

To use it, init a pool:

JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");

You can store the pool somewhere statically, it is thread-safe.
JedisPoolConfig includes a number of helpful Redis-specific connection pooling defaults. JedisPool is based on Commons Pool 2, so you may want to have a look at Commons Pool’s configuration. Please see http://commons.apache.org/proper/commons-pool/apidocs/org/apache/commons/pool2/impl/GenericObjectPoolConfig.html for more details.

这段描述重点提到了,JedisPoolConfig独享是用来配置连接池属性的,他里面只是默认初始化了几个jedis连接池中比较常用的属性值。因为JedisPool也同样是基于Commons Pool 2,所以如果要配置连接池的属性,可以完全参考Commons Pool 2的GenericObjectPoolConfig的配置说明即可。也就说JedisPool没有自己去定义多余的配置,也没有针对Commons Pool 2的配置做一些命名上的更改。
我们再看一下JedisPoolConfig的源码,你就会更加明了上面这段话的意思。

public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    // defaults to make your life with connection pool easier :)
    setTestWhileIdle(true);
    setMinEvictableIdleTimeMillis(60000);
    setTimeBetweenEvictionRunsMillis(30000);
    setNumTestsPerEvictionRun(-1);
  }
}

JedisPoolConfig继承了GenericObjectPoolConfig。默认的构造函数,设置了4个属性的值。

我们本文的重点不在于讨论这4个属性的意义,而在于讨论AbandonedConfig怎么配置。
AbandonedConfig和GenericObjectPoolConfig是两个不同的配置类。JedisPool目前的这个构造函数,支持了GenericObjectPoolConfig设置。如果需要jedis连接池能够支持遗弃对象(redis连接)的清理逻辑,那么等同于需要JedisPool支持AbandonedConfig的设置。也就是说要么JedisPool中有一个重载的构造函数可以传入AbandonedConfig,要么JedisPool能够提供一个set方法,用来设置AbandonedConfig。但是很可惜JedisPool没有提供这样的支持。也就意味着JedisPool不支持AbandonedConfig,也就不支持遗弃对象(redis连接)的清理逻辑。

JedisPool为什么不支持AbandonedConfig

你应该提出这样的疑问,毕竟JedisPool自己都说了他是基于Commons Pool 2,也就是说他的底层对象池一样是基于GenericObjectPool,但是GenericObjectPool是可以针对AbandonedConfig进行配置呀。

由于篇幅原因,这里不粘贴JedisPool的源码。我们看一下他的继承结构
JedisPool继承结构

主要原因是JedisPool并没有继承GenericObjectPool,他继承自Pool,内部维护了一个GenericObjectPool类型的internalPool属性。很像是包装模式,但是严格的从设计模式类关系分析的话不属于。因为JedisPool和GenericObjectPool并没有共同的父类或接口(Object类、一些标识性接口除外)。JedisPool只是依赖GenericObjectPool实现了自己的连接池。他从中屏蔽了他不需要的特性(AbandonedConfig)

lettuce和AbandonedConfig有关的配置

从Lettuce官网摘下来这样一段连接池的代码样例

RedisClient client = RedisClient.create(RedisURI.create(host, port));

GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport
               .createGenericObjectPool(() -> client.connect(), new GenericObjectPoolConfig());

// executing work
try (StatefulRedisConnection<String, String> connection = pool.borrowObject()) {

    RedisCommands<String, String> commands = connection.sync();
    commands.multi();
    commands.set("key", "value");
    commands.set("key2", "value2");
    commands.exec();
}

// terminating
pool.close();
client.shutdown();

参考lettuce官网:https://github.com/lettuce-io/lettuce-core/wiki/Connection-Pooling-5.1
可以看到创建一个连接池是通过ConnectionPoolSupport.createGenericObjectPool方法,最终返回的是GenericObjectPool类型。既然最终返回的就是GenericObjectPool对象,我们就可以通过调用setAbandonedConfig方法进行设置AbandonedConfig了。

总结

我们前面已经分析过AbandonedConfig的源码,知道他的主要作用是用来清理遗弃对象的。
遗弃对象的产生主要有两种可能:

  1. 对象执行的操作确实比较耗时,没有其他超时机制进行干预。比如:在数据库连接池中,一个连接执行了一个比较特别慢的sql。
  2. 对象执行的操作已经完成,但是状态没有变更。这种情况大多应该产生自编程失误(借了没还),例如:我们在使用了数据库连接池中的获取到的连接,执行完业务逻辑后,没有close。

通过我们前面对于dbcp、jedis、lettuce这三种基于common pool实现的连接池的使用分析,发现并不是所有的common pool下游框架都支持AbandonedConfig。

  • dbcp是一个数据库的连接池,数据库的遇到慢查询的几率还是相对较大的,所以很有可能出现一个连接长时间执行不释放的情况。
  • dbcp他本身诞生比较早,早于spring,当没有其他上层框架来更友好的管理连接的借用,归还的时候,编程失误的情况会很多,所以第二种情况产生的概率也会很高。

redis的出现较晚,jedis和lettuce出现时,spring已经应用非常广泛了。

  • redis是一个基于内存操作的数据库,性能非常高,qps可达5-10万。除非是技术设计上的失误导致执行了特别耗时的类keys操作等,或者请求并发量巨大,否则不会产生特别严重的阻塞。
  • jedis也许是一种乐观态度设计,认为高延迟概率很低、编程失误在测试和review阶段会被发现。
  • lettuce也许是一种自由态度设计,给了使用方更大的自由,因为直接返回了底层的GenericObjectPool,使用方可以针对其追加设置额外属性。
    • 但spring在集成lettuce时,并没有提供对AbandonedConfig的配置支持,也许spring认为,自身已经提供了更上层的api,对外能够屏蔽连接的管理机制,业务逻辑无需关心连接的借用和归还,编程失误会更进一步降低吧。
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值