0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

公用池化包Commons Pool 2

Android编程精选 来源:CSDN 2023-05-04 10:36 次阅读

			

						

在我们平常的编码中,通常会将一些对象保存起来,这主要考虑的是对象的创建成本。比如像线程资源、数据库连接资源或者 TCP 连接等,这类对象的初始化通常要花费比较长的时间,如果频繁地申请和销毁,就会耗费大量的系统资源,造成不必要的性能损失。

并且这些对象都有一个显著的特征,就是通过轻量级的重置工作,可以循环、重复地使用。这个时候,我们就可以使用一个虚拟的池子,将这些资源保存起来,当使用的时候,我们就从池子里快速获取一个即可。

Java 中,池化技术应用非常广泛,常见的就有数据库连接池、线程池等,本文主讲连接池,线程池我们将在后续的博客中进行介绍。

公用池化包 Commons Pool 2

简介

我们首先来看一下 Java 中公用的池化包 Commons Pool 2,来了解一下对象池的一般结构。根据我们的业务需求,使用这套 API 能够很容易实现对象的池化管理。



org.apache.commons
commons-pool2
2.11.1


GenericObjectPool 是对象池的核心类,通过传入一个对象池的配置和一个对象的工厂,即可快速创建对象池。

publicGenericObjectPool(
finalPooledObjectFactoryfactory,
finalGenericObjectPoolConfigconfig)

案例

Redis 的常用客户端 Jedis,就是使用 Commons Pool 管理连接池的,可以说是一个最佳实践。下图是 Jedis 使用工厂创建对象的主要代码块。对象工厂类最主要的方法就是makeObject,它的返回值是 PooledObject 类型,可以将对象使用 new DefaultPooledObject<>(obj) 进行简单包装返回。
redis.clients.jedis.JedisFactory,使用工厂创建对象。

@Override
publicPooledObjectmakeObject()throwsException{


Jedisjedis=null;
try{


jedis=newJedis(jedisSocketFactory,clientConfig);
//主要的耗时操作
jedis.connect();
//返回包装对象
returnnewDefaultPooledObject<>(jedis);
}catch(JedisExceptionje){


if(jedis!=null){


try{


jedis.quit();
}catch(RuntimeExceptione){


logger.warn("ErrorwhileQUIT",e);
}
try{


jedis.close();
}catch(RuntimeExceptione){


logger.warn("Errorwhileclose",e);
}
}
throwje;
}
}

我们再来介绍一下对象的生成过程,如下图,对象在进行获取时,将首先尝试从对象池里拿出一个,如果对象池中没有空闲的对象,就使用工厂类提供的方法,生成一个新的。

publicTborrowObject(finalDurationborrowMaxWaitDuration)throwsException{


//此处省略若干行
while(p==null){


create=false;
//首先尝试从池子中获取。
p=idleObjects.pollFirst();
//池子里获取不到,才调用工厂内生成新实例
if(p==null){


p=create();
if(p!=null){


create=true;
}
}
//此处省略若干行
}
//此处省略若干行
}

那对象是存在什么地方的呢?这个存储的职责,就是由一个叫作 LinkedBlockingDeque的结构来承担的,它是一个双向的队列。
接下来看一下 GenericObjectPoolConfig 的主要属性:

//GenericObjectPoolConfig本身的属性
privateintmaxTotal=DEFAULT_MAX_TOTAL;
privateintmaxIdle=DEFAULT_MAX_IDLE;
privateintminIdle=DEFAULT_MIN_IDLE;
//其父类BaseObjectPoolConfig的属性
privatebooleanlifo=DEFAULT_LIFO;
privatebooleanfairness=DEFAULT_FAIRNESS;
privatelongmaxWaitMillis=DEFAULT_MAX_WAIT_MILLIS;
privatelongminEvictableIdleTimeMillis=DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
privatelongevictorShutdownTimeoutMillis=DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
privatelongsoftMinEvictableIdleTimeMillis=DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
privateintnumTestsPerEvictionRun=DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
privateEvictionPolicyevictionPolicy=null;
//Only2.6.0applicationssetthis
privateStringevictionPolicyClassName=DEFAULT_EVICTION_POLICY_CLASS_NAME;
privatebooleantestOnCreate=DEFAULT_TEST_ON_CREATE;
privatebooleantestOnBorrow=DEFAULT_TEST_ON_BORROW;
privatebooleantestOnReturn=DEFAULT_TEST_ON_RETURN;
privatebooleantestWhileIdle=DEFAULT_TEST_WHILE_IDLE;
privatelongtimeBetweenEvictionRunsMillis=DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
privatebooleanblockWhenExhausted=DEFAULT_BLOCK_WHEN_EXHAUSTED;

参数很多,要想了解参数的意义,我们首先来看一下一个池化对象在整个池子中的生命周期。如下图所示,池子的操作主要有两个:一个是业务线程,一个是检测线程。

2d447c16-e897-11ed-ab56-dac502259ad0.png

对象池在进行初始化时,要指定三个主要的参数:

  • maxTotal 对象池中管理的对象上限
  • maxIdle 最大空闲数
  • minIdle 最小空闲数

其中maxTotal 和业务线程有关,当业务线程想要获取对象时,会首先检测是否有空闲的对象。如果有,则返回一个;否则进入创建逻辑。此时,如果池中个数已经达到了最大值,就会创建失败,返回空对象。

对象在获取的时候,有一个非常重要的参数,那就是最大等待时间(maxWaitMillis),这个参数对应用方的性能影响是比较大的。该参数默认为 -1,表示永不超时,直到有对象空闲。

如下图,如果对象创建非常缓慢或者使用非常繁忙,业务线程会持续阻塞 (blockWhenExhausted 默认为 true),进而导致正常服务也不能运行。

2d613162-e897-11ed-ab56-dac502259ad0.png

面试题

一般面试官会问:你会把超时参数设置成多大呢?

我一般都会把最大等待时间,设置成接口可以忍受的最大延迟。比如,一个正常服务响应时间 10ms 左右,达到 1 秒钟就会感觉到卡顿,那么这个参数设置成 500~1000ms 都是可以的。超时之后,会抛出 NoSuchElementException 异常,请求会快速失败,不会影响其他业务线程,这种 Fail Fast 的思想,在互联网应用非常广泛。

带有evcit字样的参数,主要是处理对象逐出的。池化对象除了初始化和销毁的时候比较昂贵,在运行时也会占用系统资源。比如,连接池会占用多条连接,线程池会增加调度开销等。业务在突发流量下,会申请到超出正常情况的对象资源,放在池子中。等这些对象不再被使用,我们就需要把它清理掉。

超出minEvictableIdleTimeMillis参数指定值的对象,就会被强制回收掉,这个值默认是 30 分钟;softMinEvictableIdleTimeMillis参数类似,但它只有在当前对象数量大于 minIdle 的时候才会执行移除,所以前者的动作要更暴力一些。

还有4 个 test 参数:testOnCreate、testOnBorrow、testOnReturn、testWhileIdle,分别指定了在创建、获取、归还、空闲检测的时候,是否对池化对象进行有效性检测。

开启这些检测,能保证资源的有效性,但它会耗费性能,所以默认为 false。生产环境上,建议只将testWhileIdle设置为 true,并通过调整空闲检测时间间隔(timeBetweenEvictionRunsMillis),比如 1 分钟,来保证资源的可用性,同时也保证效率。

JMH 测试

使用连接池和不使用连接池,它们之间的性能差距到底有多大呢?下面是一个简单的 JMH 测试例子(见仓库),进行一个简单的 set 操作,为 redis 的 key 设置一个随机值。

@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations=5,time=1)
@Measurement(iterations=5,time=1)
@BenchmarkMode(Mode.Throughput)
publicclassJedisPoolVSJedisBenchmark{


JedisPoolpool=newJedisPool("localhost",6379);

@Benchmark
publicvoidtestPool(){


Jedisjedis=pool.getResource();
jedis.set("a",UUID.randomUUID().toString());
jedis.close();
}

@Benchmark
publicvoidtestJedis(){


Jedisjedis=newJedis("localhost",6379);
jedis.set("a",UUID.randomUUID().toString());
jedis.close();
}
//此处省略若干行
}

将测试结果使用 meta-chart 作图,展示结果如下图所示,可以看到使用了连接池的方式,它的吞吐量是未使用连接池方式的 5 倍!

2d7f3270-e897-11ed-ab56-dac502259ad0.png

数据库连接池 HikariCP

HikariCP 源于日语“光る”,光的意思,寓意软件工作速度和光速一样快,它是 SpringBoot 中默认的数据库连接池。数据库是我们工作中经常使用到的组件,针对数据库设计的客户端连接池是非常多的,它的设计原理与我们在本文开头提到的基本一致,可以有效地减少数据库连接创建、销毁的资源消耗。

同是连接池,它们的性能也是有差别的,下图是 HikariCP 官方的一张测试图,可以看到它优异的性能,官方的 JMH 测试代码见 Github。

2da5ffae-e897-11ed-ab56-dac502259ad0.png

一般面试题是这么问的:HikariCP 为什么快呢?主要有三个方面:

  • 它使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作;
  • 优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令;
  • 实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争。

HikariCP 对性能的一些优化操作,是非常值得我们借鉴的,在之后的博客中,我们将详细分析几个优化场景。

数据库连接池同样面临一个最大值(maximumPoolSize)和最小值(minimumIdle)的问题。这里同样有一个非常高频的面试题:你平常会把连接池设置成多大呢?

很多同学认为,连接池的大小设置得越大越好,有的同学甚至把这个值设置成 1000 以上,这是一种误解。根据经验,数据库连接,只需要 20~50 个就够用了。具体的大小,要根据业务属性进行调整,但大得离谱肯定是不合适的。

HikariCP 官方是不推荐设置 minimumIdle 这个值的,它将被默认设置成和 maximumPoolSize 一样的大小。如果你的数据库Server端连接资源空闲较大,不妨也可以去掉连接池的动态调整功能。

另外,根据数据库查询和事务类型,一个应用中是可以配置多个数据库连接池的,这个优化技巧很少有人知道,在此简要描述一下。

业务类型通常有两种:一种需要快速的响应时间,把数据尽快返回给用户;另外一种是可以在后台慢慢执行,耗时比较长,对时效性要求不高。如果这两种业务类型,共用一个数据库连接池,就容易发生资源争抢,进而影响接口响应速度。虽然微服务能够解决这种情况,但大多数服务是没有这种条件的,这时就可以对连接池进行拆分。

如图,在同一个业务中,根据业务的属性,我们分了两个连接池,就是来处理这种情况的。

2db8fc58-e897-11ed-ab56-dac502259ad0.png

HikariCP 还提到了另外一个知识点,在 JDBC4 的协议中,通过 Connection.isValid() 就可以检测连接的有效性。这样,我们就不用设置一大堆的 test 参数了,HikariCP 也没有提供这样的参数。

结果缓存池

到了这里你可能会发现池(Pool)与缓存(Cache)有许多相似之处。

它们之间的一个共同点,就是将对象加工后,存储在相对高速的区域。我习惯性将缓存看作是数据对象,而把池中的对象看作是执行对象。缓存中的数据有一个命中率问题,而池中的对象一般都是对等的。

考虑下面一个场景,jsp 提供了网页的动态功能,它可以在执行后,编译成 class 文件,加快执行速度;再或者,一些媒体平台,会将热门文章,定时转化成静态的 html 页面,仅靠 nginx 的负载均衡即可应对高并发请求(动静分离)。

这些时候,你很难说清楚,这是针对缓存的优化,还是针对对象进行了池化,它们在本质上只是保存了某个执行步骤的结果,使得下次访问时不需要从头再来。我通常把这种技术叫作结果缓存池(Result Cache Pool),属于多种优化手段的综合。

小结

下面我来简单总结一下本文的内容重点:

我们从Java 中最通用的公用池化包 Commons Pool 2 说起,介绍了它的一些实现细节,并对一些重要参数的应用做了讲解;Jedis 就是在 Commons Pool 2 的基础上封装的,通过 JMH 测试,我们发现对象池化之后,有了接近 5 倍的性能提升;接下来介绍了数据库连接池中速度速快的 HikariCP ,它在池化技术之上,又通过编码技巧进行了进一步的性能提升,HikariCP 是我重点研究的类库之一,我也建议你加入自己的任务清单中。

总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:

  • 对象的创建或者销毁,需要耗费较多的系统资源;
  • 对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待;
  • 对象创建后,通过一些状态重置,可被反复使用。

将对象池化之后,只是开启了第一步优化。要想达到最优性能,就不得不调整池的一些关键参数,合理的池大小加上合理的超时时间,就可以让池发挥更大的价值。和缓存的命中率类似,对池的监控也是非常重要的。

如下图,可以看到数据库连接池连接数长时间保持在高位不释放,同时等待的线程数急剧增加,这就能帮我们快速定位到数据库的事务问题。

2dd30300-e897-11ed-ab56-dac502259ad0.png

平常的编码中,有很多类似的场景。比如 Http 连接池,Okhttp 和 Httpclient 就都提供了连接池的概念,你可以类比着去分析一下,关注点也是在连接大小和超时时间上;在底层的中间件,比如 RPC,也通常使用连接池技术加速资源获取,比如 Dubbo 连接池、 Feign 切换成 httppclient 的实现等技术。

你会发现,在不同资源层面的池化设计也是类似的。比如线程池,通过队列对任务进行了二层缓冲,提供了多样的拒绝策略等,线程池我们将在后续的文章中进行介绍。线程池的这些特性,你同样可以借鉴到连接池技术中,用来缓解请求溢出,创建一些溢出策略。现实情况中,我们也会这么做。那么具体怎么做?有哪些做法?这部分内容就留给大家思考了,欢迎你在下方留言,与大家一起分享讨论。

点也是在连接大小和超时时间上;在底层的中间件,比如 RPC,也通常使用连接池技术加速资源获取,比如 Dubbo 连接池、 Feign 切换成 httppclient 的实现等技术。


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • API
    API
    +关注

    关注

    2

    文章

    1471

    浏览量

    61742
  • TCP
    TCP
    +关注

    关注

    8

    文章

    1346

    浏览量

    78924
  • 数据库
    +关注

    关注

    7

    文章

    3750

    浏览量

    64219

原文标题:换上 HikariCP 连接池,太快了!

文章出处:【微信号:AndroidPush,微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    PostgreSQL 如何进行缓存管理

    为了提高数据访问的速度,一般数据库操作系统都会引入内存作为缓存,而为了方便管理和合并I/O,一般会开辟一个缓存(buffer pool)。
    发表于 11-01 09:41 2089次阅读
    PostgreSQL 如何进行缓存<b class='flag-5'>池</b>管理

    [分享]详细解读Hibernate的作用

    应用中根据自己的需要进行取舍。   下载Hibernate,例如2.0.3稳定版本,解压缩,可以看到一个hibernate2.jar和lib目录下有22个jar:  
    发表于 03-23 15:29

    将网格服务器添加到现有

    我目前有一个xenserver,在4台服务器上运行大约500个xendesktops。我购买了一台带有2x k1网格的额外服务器,我想将它添加到池中。我将创建一个cad系统目录,我将分配给该服
    发表于 09-05 09:35

    DevEco开发求助:找不到org.apache.commons.pool2.impl.GenericObjectPoolConfig的类文件

    华为DevEco Studio开发遇到的一个问题目前在尝试跑通网上一个开源的服务器之前有过一个报错,jedis的一个报错,导入pool2后错误解决,但是出现如下新的报错。运行报错:(如图
    发表于 08-07 15:32

    线程创建的两种方法

    ):print('running thread-{}:{}'.format(threading.get_ident(), i))time.sleep(1)# 创建一个最大容纳数量为5的线程pool
    发表于 03-16 16:15

    详解移动通信领域里的组POOL

    在移动通信领域,我们经常会提到Pool的概念。Pool,通常译为水塘、水池。在移动通信中POOL通称为“
    的头像 发表于 03-19 16:15 7810次阅读
    详解移动通信领域里的组<b class='flag-5'>POOL</b>

    比特币现金的硬分叉临近,SV-Pool已正式宣布面向矿工开放矿

    随着比特币现金协议升级的临近,相关的组织正在积极为硬分叉做准备。数据网站例如Coin Dance已经添加关于特性支持、升级投票、公众意见的统计数据。同时,Nchain支持下的SV-Pool已正式宣布面向矿工开放矿
    发表于 10-26 11:02 1473次阅读

    Linux 内存源码浅析

    内存(Memery Pool)技术是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够
    发表于 04-02 14:32 245次阅读

    如何利用阶层Stratum V2协议来改善比特币的去中心

    于2010年,自2013年以来一直运营Slush Pool。因此他们有能力设计V2协议,从而解决一些矿和矿工使用原始协议时,遇到最大的问题。
    发表于 12-10 08:49 884次阅读

    如何正确使用SpringBoot中的线程

    来自丨CSDN https://blog.csdn.net/m0_37701381/article/details/81072774 使用步骤 先创建一个线程的配置,让Spring Boot加载
    的头像 发表于 09-02 17:14 1979次阅读

    p2pool比特币挖矿

    p2pool.zip
    发表于 06-06 14:43 1次下载
    p<b class='flag-5'>2pool</b>比特币挖矿<b class='flag-5'>池</b>

    ModBus Pool下载

    ModBus Pool下载
    发表于 10-08 09:41 6次下载

    线程的线程怎么释放

    从线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程导致的问题。我们先排查为何这个线程池中会有600+的线程处于waiting状态并且无法释放,记接
    发表于 07-31 10:49 2206次阅读
    线程<b class='flag-5'>池</b>的线程怎么释放

    了解连接、线程、内存、异步请求

    技术 技术能够减少资源对象的创建次数,提⾼程序的响应性能,特别是在⾼并发下这种提⾼更加明显。使用
    的头像 发表于 11-09 14:44 1134次阅读
    了解连接<b class='flag-5'>池</b>、线程<b class='flag-5'>池</b>、内存<b class='flag-5'>池</b>、异步请求<b class='flag-5'>池</b>

    技术的应用实践

    作为一名Java开发人员,技术或多或少在业务代码中使用。常见的包括线程、连接等。也是因为Java语言超级丰富的基建,基本上这些
    的头像 发表于 11-24 10:22 466次阅读
    <b class='flag-5'>池</b><b class='flag-5'>化</b>技术的应用实践