Mari kita kaji dahulu pakej pengumpulan objek biasa Commons Pool 2 di Java untuk memahami struktur kumpulan objek umum.
Mengikut keperluan perniagaan kami, menggunakan set API ini boleh melaksanakan pengurusan pengumpulan objek dengan mudah.
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.11.1</version> </dependency>
GenericObjectPool ialah kelas teras kumpulan objek Dengan meneruskan konfigurasi kumpulan objek dan kilang objek, anda boleh membuat kumpulan objek dengan cepat.
public GenericObjectPool( final PooledObjectFactory<T> factory, final GenericObjectPoolConfig<T> config)
Pelanggan biasa Redis, Jedis menggunakan Kolam Commons untuk mengurus kumpulan sambungan, yang boleh dikatakan sebagai amalan terbaik. Angka berikut ialah blok kod utama untuk Jedis menggunakan kilang untuk mencipta objek.
Kaedah utama kelas kilang objek ialah makeObject Nilai pulangannya ialah jenis PooledObject. Objek boleh dibungkus dan dikembalikan menggunakan DefaultPooledObject baharu(obj).
redis.clients.jedis.JedisFactory, menggunakan kilang untuk mencipta objek.
@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; } }
Mari kita perkenalkan lagi proses penjanaan objek, seperti yang ditunjukkan dalam rajah di bawah Apabila memperoleh objek, ia akan cuba mengeluarkan satu daripada kumpulan objek jika tiada objek bebas , gunakan kelas kilang untuk menyediakan kaedah untuk menjana yang baharu.
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; } } //此处省略若干行 } //此处省略若干行 }
Di manakah objek itu wujud? Tanggungjawab penyimpanan ini ditanggung oleh struktur yang dipanggil LinkedBlockingDeque, yang merupakan baris gilir dua hala.
Seterusnya, mari kita lihat sifat utama 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<T> evictionPolicy = null; // Only 2.6.0 applications set this 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;
Terdapat banyak parameter Untuk memahami maksud parameter, mari kita lihat kitaran hayat a objek terkumpul di seluruh kolam.
Seperti yang ditunjukkan dalam rajah di bawah, terdapat dua operasi utama kumpulan: satu ialah rangkaian perniagaan dan satu lagi ialah rangkaian pengesanan.
Apabila memulakan kumpulan objek, tiga parameter utama mesti ditentukan:
maxTotal Had atas objek yang diuruskan dalam objek kolam
nombor melahu maksimum maxIdle
nombor melahu minimum minIdle
di mana maxTotal berkaitan dengan business threads , apabila business thread ingin mendapatkan objek, ia akan menyemak dahulu sama ada terdapat objek terbiar.
Jika ada, kembalikan satu; jika tidak, masukkan logik penciptaan. Jika saiz maksimum telah dicapai dalam kolam, penciptaan objek akan gagal dan objek kosong akan dikembalikan.
Apabila memperoleh objek, terdapat parameter yang sangat penting, iaitu masa menunggu maksimum (maxWaitMillis Parameter ini mempunyai kesan yang agak besar terhadap prestasi aplikasi). Parameter ini lalai kepada -1, yang bermaksud ia tidak akan tamat masa sehingga objek kosong.
Seperti yang ditunjukkan di bawah, jika penciptaan objek sangat perlahan atau penggunaan sangat sibuk, urutan perniagaan akan terus disekat (blockWhenExhausted lalai kepada true), yang akan menyebabkan perkhidmatan biasa gagal dijalankan.
Soalan temu bual
Saya biasanya ditanya semasa temu duga: "Berapa lama anda akan menetapkan parameter tamat masa kepada?" ditetapkan kepada kelewatan maksimum yang boleh diterima oleh antara muka.
Sebagai contoh, masa respons perkhidmatan biasa ialah kira-kira 10ms, dan jika ia mencapai 1 saat, anda akan berasa tersekat, maka parameter ini boleh ditetapkan kepada 500~1000ms.
Selepas tamat masa, pengecualian NoSuchElementException akan dilemparkan dan permintaan akan gagal dengan cepat tanpa menjejaskan urutan perniagaan lain. Idea Pantas Gagal ini digunakan secara meluas di Internet.
Parameter dengan perkataan evcit digunakan terutamanya untuk mengendalikan pengusiran objek. Operasi penciptaan dan pemusnahan objek terkumpul bukan sahaja memakan masa, tetapi juga menduduki sumber sistem semasa masa jalan.
Sebagai contoh, kumpulan sambungan akan menduduki berbilang sambungan dan kumpulan benang akan meningkatkan overhed penjadualan. Apabila perniagaan menghadapi trafik pecah, ia akan memohon sumber objek daripada kumpulan yang melebihi keperluan biasa. Apabila objek ini tidak lagi digunakan, kita perlu membersihkannya.
Objek yang melebihi nilai yang ditentukan oleh parameter minEvictableIdleTimeMillis akan dikitar semula secara paksa Nilai ini lalai kepada 30 minit parameter softMinEvictableIdleTimeMillis adalah serupa, tetapi ia hanya akan dialih keluar apabila bilangan objek semasa lebih besar daripada minIdle. , jadi tindakan bekas lebih ganas.
Terdapat juga 4 parameter ujian: testOnCreate, testOnBorrow, testOnReturn dan testWhileIdle, yang masing-masing menentukan sama ada untuk menyemak kesahihan objek terkumpul semasa penciptaan, pemerolehan, pemulangan dan pengesanan melahu.
Mendayakan semakan ini boleh memastikan keberkesanan sumber, tetapi ia akan menggunakan prestasi, jadi lalai adalah palsu.
Dalam persekitaran pengeluaran, disyorkan untuk hanya menetapkan testWhileIdle kepada benar dan melaraskan selang pengesanan melahu (timeBetweenEvictionRunsMillis), seperti 1 minit, untuk memastikan ketersediaan dan kecekapan sumber.
Berapa besar jurang prestasi antara menggunakan pengumpulan sambungan dan tidak menggunakan pengumpulan sambungan?
Berikut ialah contoh ujian JMH mudah (lihat gudang), yang menjalankan operasi set mudah untuk menetapkan nilai rawak untuk kunci redis.
@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);
@Benchmarkpublicvoid testPool() { Jedis jedis = pool.getResource(); jedis.set("a", UUID.randomUUID().toString()); jedis.close(); } @Benchmarkpublicvoid testJedis() { Jedis jedis = new Jedis("localhost",6379); jedis.set("a", UUID.randomUUID().toString()); jedis.close(); }//此处省略若干行}
将测试结果使用 meta-chart 作图,展示结果如下图所示,可以看到使用了连接池的方式,它的吞吐量是未使用连接池方式的 5 倍!
HikariCP 源于日语“光る”,光的意思,寓意软件工作速度和光速一样快,它是 SpringBoot 中默认的数据库连接池。
数据库是我们工作中经常使用到的组件,针对数据库设计的客户端连接池是非常多的,它的设计原理与我们在本文开头提到的基本一致,可以有效地减少数据库连接创建、销毁的资源消耗。
同是连接池,它们的性能也是有差别的,下图是 HikariCP 官方的一张测试图,可以看到它优异的性能,官方的 JMH 测试代码见 Github。
一般面试题是这么问的:HikariCP 为什么快呢?
主要有三个方面:
它使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作
优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令
实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争
HikariCP 对性能的一些优化操作,是非常值得我们借鉴的,在之后的博客中,我们将详细分析几个优化场景。
数据库连接池同样面临一个最大值(maximumPoolSize)和最小值(minimumIdle)的问题。一个在面试中经常被问到的问题是:你通常会将连接池设置为多大?
许多学生误以为,连接池设置得越大越好,有些学生甚至将该值设置为1000以上。
根据经验,数据库连接,只需要 20~50 个就够用了。具体的大小,要根据业务属性进行调整,但大得离谱肯定是不合适的。
HikariCP 官方是不推荐设置 minimumIdle 这个值的,它将被默认设置成和 maximumPoolSize 一样的大小。如果你的数据库Server端的连接资源空闲很多,你可以考虑禁用连接池的动态调整功能。
另外,根据数据库查询和事务类型,一个应用中是可以配置多个数据库连接池的,这个优化技巧很少有人知道,在此简要描述一下。
业务类型通常有两种:一种需要快速的响应时间,把数据尽快返回给用户;另外一种是可以在后台慢慢执行,耗时比较长,对时效性要求不高。
如果这两种业务类型,共用一个数据库连接池,就容易发生资源争抢,进而影响接口响应速度。
虽然微服务能够解决这种情况,但大多数服务是没有这种条件的,这时就可以对连接池进行拆分。
如图,在同一个业务中,根据业务的属性,我们分了两个连接池,就是来处理这种情况的。
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 是我重点研究的类库之一,我也建议你加入自己的任务清单中。
总体来说,当你遇到下面的场景,就可以考虑使用池化来增加系统性能:
对象的创建或者销毁,需要耗费较多的系统资源
对象的创建或者销毁,耗时长,需要繁杂的操作和较长时间的等待
对象创建后,通过一些状态重置,可被反复使用
将对象池化之后,只是开启了第一步优化。要想达到最优性能,就不得不调整池的一些关键参数,合理的池大小加上合理的超时时间,就可以让池发挥更大的价值。和缓存的命中率类似,对池的监控也是非常重要的。
如下图,可以看到数据库连接池连接数长时间保持在高位不释放,同时等待的线程数急剧增加,这就能帮我们快速定位到数据库的事务问题。
平常的编码中,有很多类似的场景。比如 Http 连接池,Okhttp 和 Httpclient 就都提供了连接池的概念,你可以类比着去分析一下,关注点也是在连接大小和超时时间上。
在底层的中间件,比如 RPC,也通常使用连接池技术加速资源获取,比如 Dubbo 连接池、 Feign 切换成 httppclient 的实现等技术。
你会发现,在不同资源层面的池化设计也是类似的。在后续的文章中,我们将介绍线程池的功能,例如利用队列对任务进行二层缓冲、提供多种拒绝策略等。
线程池的这些特性,你同样可以借鉴到连接池技术中,用来缓解请求溢出,创建一些溢出策略。
现实情况中,我们也会这么做。那么具体怎么做?有哪些做法?这部分内容就留给大家思考了。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ykq</groupId> <artifactId>qy151-springboot-vue</artifactId> <version>0.0.1-SNAPSHOT</version> <name>qy151-springboot-vue</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>1.9.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
Atas ialah kandungan terperinci Bagaimana untuk membuat kolam sambungan SpringBoot HikariCP. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!