数据库连接池

为什么需要连接池

当一个数据库操作任务到来时,程序需要和数据库建立连接,进行三次握手、数据库用户验证,然后执行SQL语句,最后用户退出、四次挥手关闭连接。每次任务都执行这样的流程,那么整个流程中,真正有效而且变化的只有执行SQL语句这一步骤,而且每次建立连接、用户验证、关闭连接都耗费时间。

因此,考虑能不能将连接只创建一次,然后复用长连接执行 SQL 语句呢?这需要池化技术

池化技术可以减少资源对象的创建次数,提高程序的响应性能,特别是对高并发场景下的性能提升非常明显。
适合使用池化技术缓存的资源对象具有如下特点:

  • 对象创建时间长
  • 对象占用资源多
  • 对象创建后可以重复使用

数据库连接池

数据库连接池是程序启动时建立一定数量的数据库连接,并将这些连接组成一个连接池,当程序需要用到连接去进行数据库操作的时候,直接从连接池中获取一个连接对象使用,使用完毕后,将连接对象归还给连接池。

优点:
(1)资源复用。避免了频繁的创建、释放连接引起的性能开销,减少系统消耗,增进系统运行环境的稳定(减少内存碎片和数据库临时线程/进程数量)。
(2)更快的系统响应速度。数据库连接池初始化完成后,直接利用现有可用连接,避免了从数据库连接初始化和释放过程的开销,从而缩减了系统整体响应时间。
(3)统一的连接管理,避免数据库连接泄漏。数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄露。

关键参数(以HikariCP为例)

  1. connectionTimeout:客户端等待池中连接的最大事件(毫秒),超时则会抛出 SQLException,最低可接受时间为 250ms,默认值为30000ms
  2. maximumPoolSize:连接池中的最大连接数。默认为 10
  3. minimumIdle:控制 HikariCP 中维护的最小空闲连接数。当空闲连接数小于 minimumIdle 并且池中的总连接数少于 maximumPoolSize 时,HikariCP 将添加其他连接直到 maximumPoolSize。为了获得最佳性能和对峰值需求的响应能力建议不要设置此值。 默认值与 maximumPoolSize 相同
  4. idleTimeout:池中连接保持空闲状态的最长时间,只有在定义的minimumIdle 小于maximumPoolSize时生效,允许的最小时间为 10000ms,默认为 600000ms.当你配置了minimumIdle 且它的值还和maximumPoolSize不同的时候,就相当于告诉HikariCP,你至少得给我创建minimumIdle 多的连接备着。但是当你的系统忙的把minimumIdle 个备着的连接都拿走使用时,此时再问HikariCP索要,它就得给你创建多于minimumIdle 的连接,除非有连接还回来,否则我就得一直给你创建新的,直到达到上限——maximumPoolSize。但是,当你的系统过了忙碌期,闲下来的时候,多创建出来的那些连接,HikariCP就得抽空给干掉,因为它只需要保留minimumIdle 个就好了,多了浪费。此时这个idleTimeout参数就有用了,它就是用来告诉HikariCP,多出来的这些连接,多长时间没再被使用,你就可以干掉了。
  5. connectionTestQuery: 用来测试连接是否可用的 SQL 查询,HikariCP 默认会使用 SELECT 1 语句进行测试,如果设置为null,则不会进行测试。
  6. maxLifetime: 最有可能出现坑的参数.池中连接的最大生命周期,默认值为1800000ms,即30分钟。如果设置为0,表示存活时间无限大。如果不等于0且小于30秒则会被重置回30分钟。如果设置了maxLifetime,则HikariCP给池中创建的每个连接,都会定时检测,不管这个连接上一次是什么时候使用的,只要在检测时它没在使用中,就会被淘汰。注意,这就是maxLifetime参数难以被理解的原因——哪怕是一个刚用完归还的连接,不幸碰巧赶上检测了,只能说永别——使得这个参数背后的逻辑显得非常不合理。但经过搜索,作者的想法是:一个连接即使一直能用,也不应该一直存在下去,应该定时的关闭(哪怕一天关一次),好让数据库服务器那边清理掉一些浪费的资源。所以这才是maxLifetime最根本存在的原因。

连接数设置为多少才合适

  1. 经验公式,连接数=(核心数*2)+有效磁盘数。

假如服务器CPU是i7的8核,那么连接池连接数大小为 8∗2+1=9 。这仅仅是一个经验公式,具体的还要和线程池数量以及具体业务结合在一起。

  1. IO密集型任务

如果任务整体上是一个IO密集型的任务。在处理一个请求的过程中(处理一个任务),总共耗时100+5=105ms,而其中只有5ms是用于计算操作的(消耗cpu),另外的100ms等待io响应,CPU利用率为5/(100+5)。

使用线程池是为了尽量提高CPU的利用率,减少对CPU资源的浪费,假设以100%的CPU利用率来说,要达到100%的CPU利用率,对于一个CPU就要设置其利用率的倒数个数的线程数,也即1/(5/(100+5))=21,4个CPU的话就乘以4,即84,这个时候线程池要设置84个线程数,然后连接池也是设置为84个连接。

连接池和长连接的区别

  • 长连接是一些驱动、驱动架构、ORM(即Object-Relational Mapping)工具的特性,由驱动来保持连接句柄的打开,以便后续的数据库操作可以重用连接,从而减少数据库的连接开销。

  • 连接池是应用服务器的组件,它可以通过参数来配置连接数、连接检查、连接的生命周期等。

  • 连接池内的连接,其实就是长连接。

如何简单实现一个连接池

实现一个连接池,最关键的是均衡保活.连接池的“池”通过队列数据结构进行实现,队列先进先出的特性保证了使用连接的均衡性,每一条连接都可以均匀的被使用到.连接池对外提供get()和free()两个API,get()用于从队首“出队”获取一条可用连接,free()用于将使用完的连接从对尾“入队”释放到队列中。

业务代码在低峰时会降低get()动作,所以连接池中的连接在长时间不用时会导致失效,此时保活线程在监测到get()的使用频率较低时,会模拟业务程序调用get()获取连接后发送心跳包,然后再通过free()将被保活的连接放回队列中,达到连接池中所有连接保活的目的。