数据库连接池
最后更新于
最后更新于
什么是数据库连接池? 数据库连接池和线程池是一样的,都属于池化资源,不同的是资源本身的不同,数据库连接池是对数据库连接的建立、关闭和管理,主要作用是避免频繁的创建和销毁数据库连接,因为数据库连接建立和销毁是一个重量级的操作(涉及到TCP的三次握手和四次挥手,mysql用户登陆,权限验证等,这也是会引发问题的地方);我们使用数据库的核心操作是使用一个连接执行一个或多个SQL,获取结果,所以多个SQL之间共享一个连接是可行的,数据库连接池就是这样一种池化技术,需要连接时从连接池中获取,不需要时归还给连接池,而连接的创建和关闭由连接池独立管理。 连接池的原理如下: 应用服务和数据库之间使用连接池做缓冲,应用服务在发起数据库操作时,都从连接池中获取连接、使用连接、归还连接,连接池负责连接生命周期的管理。 JDBC 规范JDBC 其实是 JDBC API,它提供了大量的接口规范,用来定义应用程序如何使用数据库。 为什么需要JDBC?我们在做业务开发时,需要用到数据库进行数据持久化,然后数据库厂商是多种多样的,每个厂商都可以开发自己的数据库应用驱动程序,但是不同厂商开发的驱动必定是多样性的,这为业务程序的开发带来了复杂性,如果我们要切换一个数据库产品,带来了维护的复杂性,所以有一套标准可以简化很多事情,而且无需过多关注驱动的实现细节,JDBC 就是这样一套标准。 JDBC 向上为 Java 的应用程序提供统一的数据库操作接口,向下屏蔽了多个数据库厂商之间的差异性,使得数据库操作标准化和流程化:
通过数据源DataSource或DriverManager获取 Connection
通过 Connection 创建 Statement
通过 Statement 执行 SQL
通过 ResultSet 获取 SQL 执行结果
迭代 ResultSet 来进行业务处理
释放 ResultSet
释放 Statement
释放 Connection
此外,JDBC 利用 SPI 机制进行服务发现,典型的面向接口编程,高内聚松耦合,彻底解耦具体的数据库驱动实现。 以 MySQL 为例,分析整个过程是如何完成的
建立数据库连接,首先进行 TCP 的三次握手,然后根据 MySQL 协议发送 Login Request,MySQL 服务器返回 OK;然后发送 SQL 语句,执行 SQL;然后返回 SQL 执行结果;最后关闭连接 conn.close(), 进行 TCP 四次挥手;整个过程耗时(在SQL不出现慢查询的情况下)100ms ~ 500 ms,可见其开销还是比较大的。
JDBC 规范本身包含连接池的规范,连接池是为了提升性能,降低复杂度而引入的池化技术
JDBC 为了屏蔽具体的数据库厂商的驱动实现,采用了 SPI 机制,叫 Service Provider Interface;具体实现是使用 ServiceLoader.load 加载实现了某个接口的所有实现类,这些实现类配置在 META-INF/services/java.sql.Driver 文件中,由实现了 JDBC 规范的驱动的jar包中指定; 其加载的本质是使用 Class.forName 的反射机制
查看 JDBC 规范了解详情:
创建和销毁连接都比较耗时,并发用户多时会让应用服务卡顿
数据库的连接是有限的,如果并发量大,数据库连接的总数会很快被用完,新的连接就会失败,同时大量的连接会占用内存和CPU资源,严重时会造成数据库性能下降,甚至宕机
我们的目的是执行一条SQL,但是产生了大量和网络IO相关的事情
频繁的创建和销毁数据库连接,会造成 JVM 临时对象增多,会触发频繁 GC
频繁关闭连接后会产生大量的 TIME_WAIT 状态的连接(在2个MSL之后关闭),不能及时回收资源,是很多问题产生的原因
并发量上来后,应用的RT会增加,QPS降低
使用了数据库连接池,主要是复用了已有的连接而不是重新建立和销毁,可以很好的解决上面的问题:
资源重用。数据库连接得到重用,减少了大量连接的创建和销毁带来的开销,也大量减少了内存碎片和数据库临时线程进程的数量
系统调优更简单,减少了对 TIME_WAIT 状态的连接的调优,需要针对连接池进行调优即可
系统响应更快。在启动应用时会初始化一定数量的连接,再加上重用连接,在执行SQL时,直接使用,大大节省了时间,系统响应更快
连接管理更灵活。连接池担当了有界缓冲,还可以自行配置连接的最小数量、最大数量、最常空闲时间、获取连接超时间、心跳检测等。另外,用户也可以结合新的技术趋势,增加数据库连接池的动态配置、监控、故障演习等一系列实用的功能
连接的测试在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 接口的精简实现,主要做了两方面的优化:
在每次调用 get() 时,去除了 rangeCheck(),节省了字节码和执行时间
在调用 remove 删除 statement 时,优化为逆序删除,也就是后add进来的先删除,这也符合connection创建statement然后关闭的顺序,相比ArrayList的顺序查找逆序删除要高效
微观层面的优化:更好的并发集合 ConcurrentBag
连接池中的连接是多线程共享的资源,既要保障线程安全,又要有极致的并发读写性能,这样才能最大限度的降低因并发操作带来的性能影响。ConcurrentBag 具有无锁设计、ThreadLocal缓存、队列窃取、直接切换优化四大特点:
ConcurrentBag 的实现并没有使用 synchronized 和 Lock,而是使用了 lock-free 算法,主要借助了几个成员变量:CopyOnWriteArrayList sharedList, SynchronousQueue handoffQueue, AtomicInteger waiters
队列窃取机制:首先尝试从ThreadLocal中获取属于当前线程的元素来避免锁竞争,如果没有可用元素则扫描公共集合,再从共享的CopyOnWriteArrayList中获取
SynchronousQueue 存在资源等待线程时的第一手资源交接
ThreadLocal 无共享设计,最大限度的重用本地线程的资源
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
HiKariCP 参数配置,使用HiKariCP 时大部分情况下是不需要修改默认的配置参数的,在高并发情况下需要进行调优
HiKariCP MySQL Configuration 可以优化 MySQL 的性能, MySQL 的手册中也有很多配置跟性能相关, 还有一个ppt
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
prepStmtCacheSize:这将设置MySQL驱动程序将为每个连接缓存的预准备语句数。默认值为保守25。我们建议将其设置为250~500。
prepStmtCacheSqlLimit:这是驱动程序将缓存的已准备SQL语句的最大长度。MySQL的默认值是256。根据我们的经验,特别是对于像Hibernate这样的ORM框架,这个默认值远低于生成的语句长度的阈值。我们推荐的设置是2048。
cachePrepStmts:如果事实上禁用了高速缓存,则上述任何参数都不会产生任何影响,因为它是默认情况下。必须将此参数设置为true。
useServerPrepStmts:较新版本的MySQL支持服务器端预处理语句,这可以提供显著的性能提升。应将此属性设置为true。
HiKariCP 监控及监控架构
参考:追光者系列 HiKariCP 连接池监控指标实战
常见问题分析
数据库连接池不是万能的,HiKariCP 同样不是万能的
HiKariCP 是最快的数据库连接池之一,但是不能把它当作万能工具,需要有更广阔的视野和更广阔的知识面。使用连接池需要注意:
多线程之间不要共享 Connection,用作局部变量是最好的
尽在需要时获取Connection,最大限度的缩短它离开池的时间;尽量先执行长时间的耗时操作,然后获取Connection,执行SQL,归还Connection
参考资料
Book: High-Performance Java Persistence
HiKariCP 数据库连接池实战
为什么数据库连接池如此重要? 数据库的连接建立需要TCP三次握手、数据库应用层协议的登陆验证过程等,连接断开需要4次挥手,可见是一个耗时长、性能低、代价高的操作,频繁的进行数据库连接的创建和销毁,在并发量高的情况下,耗时的数据库连接会让系统变得卡顿,QPS降低,RT增加;此外,数据库连接是有上限的,到达后,新的连接只能排队,甚至连接超时。那么,重用数据库连接就变得很有必要了,这样可以减少应用和数据库之间的创建和销毁TCP连接的开销。 如下是应用服务和 MySQL 数据库建立连接,执行SQL和断开连接的过程 总结一下这种方式的主要缺点:
上面是有无数据库连接池的对比,比较了4种数据库和HiKariCP连接池在打开和关闭 1000 个连接的数据对比,可见提升快 100 倍的速度 数据库连接池的原理原理:在系统初始化的时候,在内存中开辟一片空间,将一定数量的数据库连接作为对象存储在对象池里,并对外提供数据库连接的获取和归还方法。用户访问数据库时,并不是建立一个新的连接,而是从数据库连接池中取出一个已有的空闲连接对象;使用完毕归还后的连接也不会马上被关闭,而是由数据库连接池统一管理回收,为下一次借用做好准备。如果由于高并发请求导致数据库连接池中的连接被借用完毕,其他线程就会等待,直到有连接被归还。整个过程中,连接并不会被关闭,而是源源不断地循环使用,有借有还。数据库连接池还可以通过设置其参数来控制连接池中的初始连接数、连接的上下限数,以及每个连接的最大使用次数、最大空闲时间等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。 还必须再支持一些实用的功能,如并发(锁性能优化乃至无锁)、连接数控制(不同的系统对连接数有不同的需求)、监控(一些自身管理机制来监视连接的数量及使用情况等)、外部配置(各种主流数据库连接池官方文档最核心的部分)、资源重用(数据库连接池的核心思想)、检测及容灾(面对一些网络、时间等问题的自愈)、多库多服务(如不同的数据库、不同的用户名和密码、分库分表等情况)、事务处理(对数据库的操作符合ALL-ALL-NOTHING原则)、定时任务(如空闲检查、最小连接数控制)、缓存(如PSCache等避免对SQL重复解析)、异常处理(对JDBC访问的异常统一处理)、组件维护(如连接状态、JDBC封装的维护)等 数据库连接池比较 数据库中断的处理,HiKariCP 等待5秒,若连接未恢复,抛出SQLExceptions异常,后续getConnection()同样处理。HikariCP在其他方面还有很多值得称赞的地方: