数据库连接池

高性能数据库连接池内幕

HikariCP 数据库连接池

什么是数据库连接池? 数据库连接池和线程池是一样的,都属于池化资源,不同的是资源本身的不同,数据库连接池是对数据库连接的建立、关闭和管理,主要作用是避免频繁的创建和销毁数据库连接,因为数据库连接建立和销毁是一个重量级的操作(涉及到TCP的三次握手和四次挥手,mysql用户登陆,权限验证等,这也是会引发问题的地方);我们使用数据库的核心操作是使用一个连接执行一个或多个SQL,获取结果,所以多个SQL之间共享一个连接是可行的,数据库连接池就是这样一种池化技术,需要连接时从连接池中获取,不需要时归还给连接池,而连接的创建和关闭由连接池独立管理。 连接池的原理如下: 应用服务和数据库之间使用连接池做缓冲,应用服务在发起数据库操作时,都从连接池中获取连接、使用连接、归还连接,连接池负责连接生命周期的管理。 JDBC 规范JDBC 其实是 JDBC API,它提供了大量的接口规范,用来定义应用程序如何使用数据库。 为什么需要JDBC?我们在做业务开发时,需要用到数据库进行数据持久化,然后数据库厂商是多种多样的,每个厂商都可以开发自己的数据库应用驱动程序,但是不同厂商开发的驱动必定是多样性的,这为业务程序的开发带来了复杂性,如果我们要切换一个数据库产品,带来了维护的复杂性,所以有一套标准可以简化很多事情,而且无需过多关注驱动的实现细节,JDBC 就是这样一套标准。 JDBC 向上为 Java 的应用程序提供统一的数据库操作接口,向下屏蔽了多个数据库厂商之间的差异性,使得数据库操作标准化和流程化:

  1. 通过数据源DataSource或DriverManager获取 Connection

  2. 通过 Connection 创建 Statement

  3. 通过 Statement 执行 SQL

  4. 通过 ResultSet 获取 SQL 执行结果

  5. 迭代 ResultSet 来进行业务处理

  6. 释放 ResultSet

  7. 释放 Statement

  8. 释放 Connection

此外,JDBC 利用 SPI 机制进行服务发现,典型的面向接口编程,高内聚松耦合,彻底解耦具体的数据库驱动实现。 以 MySQL 为例,分析整个过程是如何完成的

  1. 建立数据库连接,首先进行 TCP 的三次握手,然后根据 MySQL 协议发送 Login Request,MySQL 服务器返回 OK;然后发送 SQL 语句,执行 SQL;然后返回 SQL 执行结果;最后关闭连接 conn.close(), 进行 TCP 四次挥手;整个过程耗时(在SQL不出现慢查询的情况下)100ms ~ 500 ms,可见其开销还是比较大的。

  2. JDBC 规范本身包含连接池的规范,连接池是为了提升性能,降低复杂度而引入的池化技术

  3. JDBC 为了屏蔽具体的数据库厂商的驱动实现,采用了 SPI 机制,叫 Service Provider Interface;具体实现是使用 ServiceLoader.load 加载实现了某个接口的所有实现类,这些实现类配置在 META-INF/services/java.sql.Driver 文件中,由实现了 JDBC 规范的驱动的jar包中指定; 其加载的本质是使用 Class.forName 的反射机制

查看 JDBC 规范了解详情:

为什么数据库连接池如此重要? 数据库的连接建立需要TCP三次握手、数据库应用层协议的登陆验证过程等,连接断开需要4次挥手,可见是一个耗时长、性能低、代价高的操作,频繁的进行数据库连接的创建和销毁,在并发量高的情况下,耗时的数据库连接会让系统变得卡顿,QPS降低,RT增加;此外,数据库连接是有上限的,到达后,新的连接只能排队,甚至连接超时。那么,重用数据库连接就变得很有必要了,这样可以减少应用和数据库之间的创建和销毁TCP连接的开销。 如下是应用服务和 MySQL 数据库建立连接,执行SQL和断开连接的过程 总结一下这种方式的主要缺点:

  1. 创建和销毁连接都比较耗时,并发用户多时会让应用服务卡顿

  2. 数据库的连接是有限的,如果并发量大,数据库连接的总数会很快被用完,新的连接就会失败,同时大量的连接会占用内存和CPU资源,严重时会造成数据库性能下降,甚至宕机

  3. 我们的目的是执行一条SQL,但是产生了大量和网络IO相关的事情

  4. 频繁的创建和销毁数据库连接,会造成 JVM 临时对象增多,会触发频繁 GC

  5. 频繁关闭连接后会产生大量的 TIME_WAIT 状态的连接(在2个MSL之后关闭),不能及时回收资源,是很多问题产生的原因

  6. 并发量上来后,应用的RT会增加,QPS降低

使用了数据库连接池,主要是复用了已有的连接而不是重新建立和销毁,可以很好的解决上面的问题:

  1. 资源重用。数据库连接得到重用,减少了大量连接的创建和销毁带来的开销,也大量减少了内存碎片和数据库临时线程进程的数量

  2. 系统调优更简单,减少了对 TIME_WAIT 状态的连接的调优,需要针对连接池进行调优即可

  3. 系统响应更快。在启动应用时会初始化一定数量的连接,再加上重用连接,在执行SQL时,直接使用,大大节省了时间,系统响应更快

  4. 连接管理更灵活。连接池担当了有界缓冲,还可以自行配置连接的最小数量、最大数量、最常空闲时间、获取连接超时间、心跳检测等。另外,用户也可以结合新的技术趋势,增加数据库连接池的动态配置、监控、故障演习等一系列实用的功能

上面是有无数据库连接池的对比,比较了4种数据库和HiKariCP连接池在打开和关闭 1000 个连接的数据对比,可见提升快 100 倍的速度 数据库连接池的原理原理:在系统初始化的时候,在内存中开辟一片空间,将一定数量的数据库连接作为对象存储在对象池里,并对外提供数据库连接的获取和归还方法。用户访问数据库时,并不是建立一个新的连接,而是从数据库连接池中取出一个已有的空闲连接对象;使用完毕归还后的连接也不会马上被关闭,而是由数据库连接池统一管理回收,为下一次借用做好准备。如果由于高并发请求导致数据库连接池中的连接被借用完毕,其他线程就会等待,直到有连接被归还。整个过程中,连接并不会被关闭,而是源源不断地循环使用,有借有还。数据库连接池还可以通过设置其参数来控制连接池中的初始连接数、连接的上下限数,以及每个连接的最大使用次数、最大空闲时间等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。 还必须再支持一些实用的功能,如并发(锁性能优化乃至无锁)、连接数控制(不同的系统对连接数有不同的需求)、监控(一些自身管理机制来监视连接的数量及使用情况等)、外部配置(各种主流数据库连接池官方文档最核心的部分)、资源重用(数据库连接池的核心思想)、检测及容灾(面对一些网络、时间等问题的自愈)、多库多服务(如不同的数据库、不同的用户名和密码、分库分表等情况)、事务处理(对数据库的操作符合ALL-ALL-NOTHING原则)、定时任务(如空闲检查、最小连接数控制)、缓存(如PSCache等避免对SQL重复解析)、异常处理(对JDBC访问的异常统一处理)、组件维护(如连接状态、JDBC封装的维护)等 数据库连接池比较 数据库中断的处理,HiKariCP 等待5秒,若连接未恢复,抛出SQLExceptions异常,后续getConnection()同样处理。HikariCP在其他方面还有很多值得称赞的地方:

  • 连接的测试在getConnection时同步进行,并有独特的优化。

  • 在自己的事务中封装内部池的查询,含testQuery一级initSQLQuery。

  • 当连接Connections归还给连接池的时候,执行rollback操作。

  • 在Connection.close的时候,跟踪并关闭废弃语句Statements。

  • 在将Connection连接归还到客户端之前清除SQL警告。

  • 默认参数支持自动提交、事务隔离级别、catalog和只读状态。

  • 对于一些诸如“SQLException对象是否存在断开连接错误”等陷阱进行检查

HiKariCP 为何如此快?HiKariCP GitHub 上有 HiKariCP 的基准测试,这里是 HiKariCP 所做的优化:Down the Rabbit HoleHiKariCP 如此受欢迎的原因是其极致的性能,同时还保证了正确性和可靠性。 主要有如下优化

  • 字节码级别的优化;优化字节码输出,精简字节码,优化拦截器

这个优化主要是使用 Javassist 生成动态代理,编译时自动生成代理类的字节码文件,使得生成的字节码更少,性能更好;此外,针对 JIT 做了有优化,HikariCP在精简字节码的时候,研究了编译器的字节码输出,甚至是JIT的汇编输出,以将关键部分限制为小于JIT内联阈值,展平了继承层次结构,阴影成员变量,消除了强制转换。

  • 微观层面的优化:FastList 替换 ArrayList

FastList 是对 List 接口的精简实现,主要做了两方面的优化:

  1. 在每次调用 get() 时,去除了 rangeCheck(),节省了字节码和执行时间

  2. 在调用 remove 删除 statement 时,优化为逆序删除,也就是后add进来的先删除,这也符合connection创建statement然后关闭的顺序,相比ArrayList的顺序查找逆序删除要高效

  • 微观层面的优化:更好的并发集合 ConcurrentBag

连接池中的连接是多线程共享的资源,既要保障线程安全,又要有极致的并发读写性能,这样才能最大限度的降低因并发操作带来的性能影响。ConcurrentBag 具有无锁设计、ThreadLocal缓存、队列窃取、直接切换优化四大特点:

  1. ConcurrentBag 的实现并没有使用 synchronized 和 Lock,而是使用了 lock-free 算法,主要借助了几个成员变量:CopyOnWriteArrayList sharedList, SynchronousQueue handoffQueue, AtomicInteger waiters

  2. 队列窃取机制:首先尝试从ThreadLocal中获取属于当前线程的元素来避免锁竞争,如果没有可用元素则扫描公共集合,再从共享的CopyOnWriteArrayList中获取

  3. SynchronousQueue 存在资源等待线程时的第一手资源交接

  4. ThreadLocal 无共享设计,最大限度的重用本地线程的资源

  5. CopyOnWriteArrayList 负责存放ConcurrentBag中全部用于出借的资源, 只有在 add 和 remove 时才复制一份数据进行修改

HiKariCP 原理分析 以正确的姿态在项目中使用 HiKariCPHiKariCP 被作为 SpringBoot2.x 的默认数据库连接池SpringBoot 2.x:HikariCP →Tomcat pool → Commons DBCP2SpringBoot 1.x:Tomcat pool → HikariCP → Commons DBCP → Commons DBCP2

jdbcUrl=jdbc:mysql://localhost:3306/simpsons username=test password=test dataSource.cachePrepStmts=true dataSource.prepStmtCacheSize=250 dataSource.prepStmtCacheSqlLimit=2048 dataSource.useServerPrepStmts=true dataSource.useLocalSessionState=true dataSource.rewriteBatchedStatements=true dataSource.cacheResultSetMetadata=true dataSource.cacheServerConfiguration=true dataSource.elideSetAutoCommits=true dataSource.maintainTimeStats=false

  1. prepStmtCacheSize:这将设置MySQL驱动程序将为每个连接缓存的预准备语句数。默认值为保守25。我们建议将其设置为250~500。

  2. prepStmtCacheSqlLimit:这是驱动程序将缓存的已准备SQL语句的最大长度。MySQL的默认值是256。根据我们的经验,特别是对于像Hibernate这样的ORM框架,这个默认值远低于生成的语句长度的阈值。我们推荐的设置是2048。

  3. cachePrepStmts:如果事实上禁用了高速缓存,则上述任何参数都不会产生任何影响,因为它是默认情况下。必须将此参数设置为true。

  4. useServerPrepStmts:较新版本的MySQL支持服务器端预处理语句,这可以提供显著的性能提升。应将此属性设置为true。

  • HiKariCP 监控及监控架构

参考:追光者系列 HiKariCP 连接池监控指标实战

  • 常见问题分析

  • 数据库连接池不是万能的,HiKariCP 同样不是万能的

HiKariCP 是最快的数据库连接池之一,但是不能把它当作万能工具,需要有更广阔的视野和更广阔的知识面。使用连接池需要注意:

  1. 多线程之间不要共享 Connection,用作局部变量是最好的

  2. 尽在需要时获取Connection,最大限度的缩短它离开池的时间;尽量先执行长时间的耗时操作,然后获取Connection,执行SQL,归还Connection

参考资料

最后更新于

这有帮助吗?