数据库连接池性能优化,连接数到底应该设置多⼤?
⽂章⽬录
1. 数据库连接池与 ThreadLocal
数据库连接池是线程安全的,但数据库连接不是!
数据库连接池就⽤⽤来保存数据库连接的⼀个池⼦。每当我们的业务代码需要和数据库进⾏交互时,就从这个池⼦⾥⾯取出⼀个数据库连接,然后在这个连接上进⾏查增删改操作。使⽤结束后,业务代码再将这个连接归还给这个池⼦,然后这个连接就可以被其他业务代码继续使⽤了。
数据库连接池本⾝的设计,也避免了多个线程同时共享⼀个连接的情况,⼀个连接必须先向连接池申请才能获得,使⽤结束必须归还连接池才能给下⼀个线程使⽤。从这个过程中我们可以看到,数据库连接池是可以在多个线程中使⽤的,因此数据库连接池必定是线程安全的。
然⽽数据库连接Connection肯定不是线程安全的,如JDBC的对象Connection, Statement / PreparedStatement 或 ResultSet 都不是线程安全的,也不是资源安全的。不能在多个线程中共享这些对象。假如所有请求servlet的连接,使⽤的都是⼀个Connection,这个就是很致命的了,多个⼈使⽤同⼀个连接,算上延迟啥的,天知道数据会成什么样!
数据库连接池为什么使⽤ThreadLocal?
Thread内部有⼀个ThreadLocalMap对象,类似⼀个map,key为ThreadLocal ,value为设置的值。
因此我们要保证Connection对每个线程都是唯⼀的,这个时候就可以⽤到ThreadLocal了,保证每个线程都有⾃⼰的连接,这样就互相不⼲扰了。
另外,假如在⼀个事务⽅法中,涉及了多个DAO操作,如果不使⽤ThreadLocal,那么其中⼀个DAO操作执⾏完毕就会关闭连接,下⼀个DAO还会从池中获取数据库连接,两个 DAO 就⽤到了两个Connection,这样的话是没有办法完成⼀个事务的。如果是从 ThreadLocal 中获得 Connection 的话,那么这些 DAO操作 就会被纳⼊到同⼀个 Connection 之下,多个dao操作使⽤的都是⼀个数据库连接,这样就可以更好地控制事务!
ThreadLocal存在的问题:内存泄漏:
当在线程池中使⽤ThreadLocal 时,可能会导致内存泄漏问题:因为线程池的任务⼀般不会被销毁,当⼀个线程执⾏完任务后,紧接着会执⾏下⼀个任务,那么在第⼀个任务中被设置的ThreadLocal 就永远不会使⽤了,但是线程没有销毁,该对象也不会被回收,这就发⽣了内存泄漏。当任务很多时,每个任务都会⽣成ThreadLocal对象,且都不会被销毁,这就会逐渐侵蚀剩余的内存空间,导致可⽤内存越来越少!
解决⽅案:为当ThreadLocal对象使⽤完之后,应该要把设置的key,value,也就是Entry对象进⾏回收,需要显式的调
⽤ve()⽅法,删除对应的数据,释放空间
2. 数据库连接数测试
主题:视频中对Oracle数据库进⾏压⼒测试,9600并发线程进⾏数据库操作,每两次访问数据库的操作之间sleep 550ms
1. ⼀开始设置的线程池⼤⼩为2048。每个请求要在连接池队列⾥等待33ms,获得连接后执⾏SQL(吞吐量)需要77ms
2. 然后设置线程池⼤⼩为1024。能看到,中间件连接池从2048减半之后,连接池队列⾥等待事件没怎么变,但执⾏SQL耗时减少了⼀
半。
3. 接下来,设置线程池⼤⼩为96,并发线程数仍然是9600不变。连接池队列⾥等待事件⼏乎没了,吞吐量被优化到了2ms。查询性能druid连接池配置详解
得到了巨⼤提升!
没有调整任何其他东西,仅仅只是缩⼩了中间件层的数据库连接池,就把请求响应时间从77ms左右缩短到了2ms。 还有同样的案例:nginx只⽤4个线程发挥出的性能就⼤⼤超越了100个进程的Apache HTTPD。这是为什么呢?
3. 透过现象看原理
从上⾯的测试,我们发现数据库连接数越⼩,性能越好。下⾯来探讨⼀下现象下的本质。
即使是单核CPU的计算机也能“同时”运⾏数百个线程。但我们都知道这只不过是操作系统⽤时间分⽚玩的⼀个⼩把戏。⼀颗CPU核⼼同⼀时刻只能执⾏⼀个线程,然后操作系统切换上下⽂,核⼼开始执⾏另⼀个线程的代码,以此类推。给定⼀颗CPU核⼼,其顺序执⾏A 和B永远⽐通过 时间分⽚“同时”执⾏A和B要快,因为顺序执⾏不需要上下⽂切换,上下⽂切换也是耗时的。这是⼀条计算机科学的基本
法则。⼀旦线程的数量超过了CPU核⼼的数量,再增加线程数系统就只会更慢,⽽不是更快。
上⾯的说法只能说是接近真理,但还并没有这么简单,有⼀些其他的因素需要加⼊。当我们寻数据库的性能瓶颈时,总是可以将其归为三类:CPU、磁盘、⽹络。把内存加进来也没有错,但⽐起磁盘和⽹络,内存的带宽要⾼出好⼏个数量级,所以就先不加了。
如果我们⽆视磁盘和⽹络,那么结论就⾮常简单。在⼀个8核的服务器上,设定连接/线程数为8能够提供最优的性能,再增加连接数就会因上下⽂切换的损耗导致性能下降。但数据库通常把数据存储在磁盘上,磁盘⼜通常是由⼀些旋转着的⾦属碟⽚和⼀个装在步进马达上的读写头组成的。它必须“寻址”到另外⼀个位置来执⾏另⼀次读写操作。所以就有了寻址的耗时,此外还有旋回耗时,读写头需要等待碟⽚上的⽬标数据“旋转到位”才能进⾏操作。在磁盘寻址这⼀时间段(即"I/O等待")内,线程是在“阻塞”着等待磁盘,此时操作系统可以将那个空闲的CPU核⼼⽤于服务其他线程。所以,由于线程总是在I/O上阻塞,我们可以让线程/连接数⽐CPU核⼼多⼀些,这样能够在同样的时间内完成更多的⼯作。
那么让线程/连接数⽐CPU核⼼多多少呢?这要取决于磁盘。较新型的SSD不需要寻址,也没有旋转的碟⽚。可别想当然地认
为“SSD速度更快,所以我们应该增加线程数”,恰恰相反,⽆需寻址和没有旋回耗时意味着更少的阻塞,所以更少的线程,更接近于CPU核⼼数会发挥出更⾼的性能。只有当阻塞创造了更多的执⾏机会
时,更多的线程数才能发挥出更好的性能。
4. 如何合理设置数据库连接数
明⽩了原理后,就可以合理的设置连接数来优化性能。
计算公式:连接数 = (核⼼数 * 2) + 有效磁盘数
按这个公式,你的4核i7数据库服务器的连接池⼤⼩应该为((4 * 2) + 1) = 9。取个整就算是是10吧。不要觉得太⼩,它能轻松搞定3000⽤户以6000TPS的速率并发执⾏简单查询的场景。如果连接池⼤⼩超过10,你会看到响应时长开始增加,TPS开始下降。因为随着连接数增多,cpu上下⽂切换频繁,增加请求耗时,⽽遵循这个公式,可以在当前线程进⾏IO阻塞时,适当的就⾏上下⽂切换,避免了切换频繁带来的耗时!*当然,这也不是⼀个万能公式,具体设置的值还需要进⾏实际压⼒测试才能决定!
在上⾯Oracle的视频中,他们把连接数从2048降到了96,提升了⼤量想能。实际上96都太⾼了,除⾮服务器有16或32颗核⼼。结论:尽量设置⼀个⼩的连接池,和⼀个充满了等待连接的线程的队列
5. Druid连接池关闭不活跃连接时抛 Connection timed out 异常!
⽣产环境下,时不时打印下⾯这个异常,但不影响主业务
解决⽅案
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.keep-alive=true
原因如下:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论