数据库连接池
不使用连接池时
我们每次操作数据库,都需要先与建立连接,操作完成之后断开连接
-
- 建立连接是一个耗时的操作,每次大约花费50ms左右,另外系统需要分配内存资源。
- 当请求并发量很高时,频繁的进行数据库连接操作必然会占用很多的系统资源,增加请求耗时
- 另外,如果程序出现异常,获取的连接没有断开,会造成内存泄漏
- 其次,每次请求都与数据库建立连接,会导致连接数不可控,如果连接过多,也会导致CPU处理不过来造成请求阻塞,还有内存问题
连接池原理
数据库连接时一种稀缺的资源,因此我们要对连接进行妥善管理。
控制连接的总数,复用连接,共享连接资源,避免每次使用都建立连接和断开连接带来的开销
对于连接的管理可使用空闲池。即把已经创建但尚未分配出去的连接按创建时间存放到一个空闲池中
当用户请求一个连接时,系统首先检查空闲池有没有空闲连接
如果有,就获取建立时间最长的连接,先对连接做有效性判断,如果可用就将此连接分配出去
如果不可用,将此连接从空闲池中删除,重新检测空闲池是否还有连接,接着做有效性判断...
如果空闲池没有连接,则检查当前活动连接是否达到连接池所允许的最大连接数(maxActive)
如果没有达到,则新建一个连接
如果已经达到,就等待一段时间(timeout)其他连接的释放
如果在等待的时间内有连接被释放出来就可以将此连接分配给等待的用户
如果等待时间超过预定时间timeout仍没有连接被释放,则返回空值null,此时程序获取不到连接会报异常
系统会对已经分配出去的连接进行计数,使用完成归还后放进空闲池,另外会对空闲池中连接的状态定时做有效性检测,也可以在从空闲池中取连接时再去做有效性检测
误区:
数据库连接池中连接的数量不是越大约好,并不是越大数据库的性能越高、吞吐量越高
性能测试:
先看一个连接池越大反而性能越低的例子(前提:单机数据库一般承受的QPS在1000):
Oracle性能小组发布的连接池大小性能测试,假设并发量为1万。模拟了9600个并发线程来操作数据库,每两次数据库操作之间sleep 550ms,测试用例及结果为:
1):连接池数为2048,结果每个请求要在连接池队列中等待33ms,获得连接之后,执行SQL耗时77ms,CPU消耗在95%左右。
2):连接池数为1024,结果每个请求要在连接池队列中等待38ms,获得连接之后,执行SQL耗时30ms,耗时减少很多。
两次比较结果为吞吐量基本没变,但是连接池数减半之后wait事件也减少了一半。
3):连接池数为96,结果每个请求在连接池队列中平均等待时间为1ms,SQL执行耗时为2ms。吞吐量大大提高。
这是因为一核的CPU同一时刻只能执行一个线程,多个线程并发执行的话操作系统为每个线程分配时间片,然后快速切换时间片,执行其他线程,不停反复,给我们造成所有线程同时运行的假象。
因此单核CPU顺序执行AB两个线程永远比并发切换时间片执行AB要快!
一旦线程的数量超过了 CPU 核心的数量,再增加线程数系统就只会更慢,而不是更快,因为这里涉及到上下文切换耗费的额外的性能。
其他影响性能的因素
1)CPU
2)磁盘IO
3)网络IO
CPU:
暂不考虑磁盘IO和网络IO,只看CPU的话,在一个8核的服务器上,数据库连接数&线程数设置为8(与核心相同)能够提供最优的性能,如果再增加连接数,反而会因为上下文切换导致性能下降。
磁盘IO:
数据库通常把数据存储在磁盘上,磁盘读写寻址的时候,线程需要阻塞等待着磁盘(IO等待),此时操作系统可以将那个空闲的CPU核心用于服务其他线程。所以,由于线程总是在IO上阻塞,我们可以让线程/连接数比CPU核心多一些,这样能够在同样的时间内完成更多的工作。
较新型的SSD不需要寻址,也没有旋转的碟片。但是别认为应该增加更多的线程数,因为无需寻址和没有旋回耗时意味着更少的阻塞(CPU不会因阻塞而空闲),所以更少的线程【更接近于CPU核心数】会发挥出更高的性能。只有当阻塞创造了更多的执行机会时(CPU因阻塞而空闲),更多的线程数才能发挥出更好的性能。
网络IO:
网络和磁盘类似,通常以太网接口读写数据时也会形成阻塞,10G带宽会比1G带宽的阻塞少一些,1G带宽又会比100M带宽的阻塞少一些。不过网络通常是放在第三位考虑的,有些人会在性能计算中忽略它们。
总结
寻找最合适的连接数可以参考下面这个公式:
连接数 =(核心数*2)+ 有效磁盘数
一般服务器的磁盘个数都是1,因此CPU为4核的数据库服务器的连接池大小应该为(4*2)+1=9,取整为10。
根据性能压力测试,这个值能轻松搞定3000用户以6000TPS的速率并发执行查询的场景,如果连接数超过10,就会看到响应时长开始增加,TPS开始下降。
即连接池中的连接数量应该等于你的数据库能够有效同时进行的查询任务数(通常不会高于2*CPU核心数)
注:这一公式其实不仅适用于数据库连接池的计算,大部分涉及计算和I/O的程序,线程数的设置都可以参考这一公式。
我们需要的是一个小连接池和一个大的饱和等待连接的线程的队列
如果并发数为10000,我们需要一个大小为10的连接池,然后让剩下的业务线程在队列里等待就可以了。
附:DBCP数据源配置
@Bean public BasicDataSource globalDataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl(jdbc:mysql://xxx:3306/xxx); dataSource.setUsername(xxx); dataSource.setPassword(xxx); // 分配的最大连接数(推荐2N,N为数据库服务器cpu核心数),负值表示无限制 dataSource.setMaxActive(10); dataSource.setMaxIdle(8); dataSource.setMinIdle(8); dataSource.setMaxWait(6000); // DBA推荐连接池配置 // 用来验证连接是否生效的sql语句 dataSource.setValidationQuery("SELECT 1"); // 从池中获取连接前进行验证 dataSource.setTestOnBorrow(true); // 向池中还回连接前进行验证 dataSource.setTestOnReturn(false); // 连接空闲时验证 dataSource.setTestWhileIdle(true); // 运行判断连接超时任务(evictor)的时间间隔,单位为毫秒,默认为-1,即不执行任务 dataSource.setTimeBetweenEvictionRunsMillis(40000); // 池中的连接空闲xxx毫秒后被回收,单位是毫秒,默认值为1800000,也就是30分钟 dataSource.setMinEvictableIdleTimeMillis(50000); return dataSource; }
参考:https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
如何设计一个数据库连接池?
1、连接池中的连接数
连接数 =(核心数*2)+ 有效磁盘数
我们需要的是一个小连接池和一个大的饱和等待连接的线程的队列
2、数据库请求获取连接过程
前提:初始化连接池时,需要指定最大连接数和最小连接数
①:连接池当前连接数<最小连接数: 创建新链接处理数据库请求
②:最小连接数 < 连接池当前连接数 < 最大连接数: 优先复用空闲连接,否则创建新连接处理请求
③:连接池连接数 > 最大连接数: 等待一段时间(自旋/线程休眠),超时还没有连接可以直接抛错
3、保证连接可用性
①:心跳机制,定期检查连接是否可用
②:每次使用连接前,先检验下连接是否可用,再进行SQL请求
性能优良且稳定性高的连接池
apache commons-dbcp:更新速度很慢
c3p0:已经很久无更新,基本处于不活跃状态
Driud:活跃更新状态
HikariCP:活跃更新状态(推荐)
END.