线程池如何保证数据安全(使用数据库连接池和线程池如何实现高并发场景)
线程池如何保证数据安全(使用数据库连接池和线程池如何实现高并发场景)从抓包分析,我们可以看出web服务器与数据库建立一次连接消耗了4ms。我再来统计一段SQL执行的时间,发现一条SQL执行时间为1ms。数据量不大的时候,建立连接和执行SQL的时间都可忽略,毕竟都是毫秒级别的。但是如果数据量一旦上来了,那么就得重视了。如果暗战执行一条SQL建立一次连接的方式的调度的化,那么1s能执行200条SQL,而建立连接就消耗了4/5。我用"tcpdump -i bond0 -nn -tttt port 4490"命令抓取了线上 MySQL 建立连接的网络包来做分析,从抓包结果来看,整个 MySQL 的连接过程可以分为两部分:这个架构图是我们每个人最熟悉的,最简单的架构原型,很多系统在一开始都是长这样的,只是随着业务复杂度的提高,架构做了叠加,然后看起来就越来越复杂了。系统上线之初,流量较低,系统稳定运行,但是随着系统的流量增大,系统访问速度变慢了。分析
- 池化思想
- 为什么使用数据库连接池
- 高并发下线程池使用
- 线程池和连接池的共同点
- 总结
池化思想
什么是池化思想?
池化思想,指的是对对象的池化,其中对象包括了:数据库连接,线程,还有就是我平时使用new关键字创建的普通的对象等等。
池化思想的本质是空间换时间,期望用预先创建好的对象来减少频繁创建对象带来的性能开销,除此之外,对象的管理与监控等等皆为池化技术所作做一些增强,例如:对象重用,对象管理,对象实时监控,伸缩性等等,这些增强特性可以从池对象的成员属性去分析。
为什么使用数据库连接池
假设我们有一个面向某垂直领域的电商系统,前端有一台服务器运行业务逻辑,后端一台数据库服务器存储业务数据,其简单架构如下:
这个架构图是我们每个人最熟悉的,最简单的架构原型,很多系统在一开始都是长这样的,只是随着业务复杂度的提高,架构做了叠加,然后看起来就越来越复杂了。系统上线之初,流量较低,系统稳定运行,但是随着系统的流量增大,系统访问速度变慢了。
分析程序的日志之后,你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。这种调用方式下,每次执行 SQL 都需要重新建立连接,所以你怀疑,是不是频繁地建立数据库连接耗费时间长导致了访问慢的问题。
那么为什么频繁创建连接会造成响应时间慢呢?此时不妨使用抓包工具来分析:
我用"tcpdump -i bond0 -nn -tttt port 4490"命令抓取了线上 MySQL 建立连接的网络包来做分析,从抓包结果来看,整个 MySQL 的连接过程可以分为两部分:
- 第一部分是前三个数据包。第一个数据包是客户端向服务端发送的一个“SYN”包,第二个包是服务端回给客户端的“ACK”包以及一个“SYN”包,第三个包是客户端回给服务端的“ACK”包,熟悉 TCP 协议的同学可以看出这是一个 TCP 的三次握手过程。(抓包工具真相,一分析就明白什么是三次握手了)
- 第二部分是 MySQL 服务端校验客户端密码的过程。其中第一个包是服务端发给客户端要求认证的报文,第二和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是服务端回给客户端认证 OK 的报文。从图中,你可以看到整个连接过程大概消耗了 4ms(969012-964904)。
从抓包分析,我们可以看出web服务器与数据库建立一次连接消耗了4ms。我再来统计一段SQL执行的时间,发现一条SQL执行时间为1ms。数据量不大的时候,建立连接和执行SQL的时间都可忽略,毕竟都是毫秒级别的。但是如果数据量一旦上来了,那么就得重视了。如果暗战执行一条SQL建立一次连接的方式的调度的化,那么1s能执行200条SQL,而建立连接就消耗了4/5。
那么这时候,我们是不是就要考虑使用线程池了?
当接入线程池之后,你会发现1s就可以执行1000次的数据查询,查询性能大大提升。
那么线程池又是怎么做到的呢?
其实再开发中,我们经常会使用到连接池,比如数据库连接池,HTTPClient连接池,Readis连接池等等。而我们要了解连接池的核心,就要从连接池的管理出发。我们就以数据库连接池为例,说明一下连接池的关键点。
数据库连接池中有两个重要的核心配置:最小连接数和最大连接数。他们控制着从连接池中获取连接的流程。连接池执行流程:
1、如果当前连接数小于最小连接数,则创建新的连接处理数据库请求(虽然预先预热创建过连接,但是
如果预热创建的连接还没被全部使用,说明当前并发量不大,可以直接创建连接,预热连接留给下次
流量高峰期时使用)
2、当前连接数大于最小连接数,小于最大连接数,且有空闲连接时,(所谓的空闲连接指的是,当前连接大于最小连
接数时创建连接,处理完请求后并没有销毁,而是保留在连接池中,此时当前连接并没有达到最大连接数。)
3、当前连接数大于最小连接数,小于最大连接数,且没有空闲连接时,则创建新的连接处理数据库请求。
4、当前连接大于最大连接数时,则按照配置的超时时间等待旧的线程可以,
5、如果等待超出这个时间,则向用户抛出错误
数据库连接池在线上一般最小连接数为10,最大连接数设置在20-30左右,后续根据自己的系统进行调试。同时,在数据库连接池的使用过程有可能会遇到如下问题:
1、数据库服务器IP发生了变化,连接池还是使用旧的IP,那么使用旧的查询,就会出现问题。
2、数据库服务器中有个参数“wait_timeout"(mysql中),当连接空闲时间超过这个时间,MySQL服务器
会单方面关闭这个连接,数据库使用方是无法感知,当我们继续使用这个连接就会出现问题
那么我们该如何保证每次从数据库连接池中每次获取的连接是可用的呢?
1、启动一个线程定期监测连接池中的连接是否可用,比如,使用该连接向数据库发送”select 1“命令
看看数据库是否会报错,如果报错,将该连接移除连接池,并关闭。(Druid和C3P0目前都使用这种方式
监测连接是否可用)
2、在获取连接之后,会校验连接是否可用,如果可用在执行SQL。在C3P0中使用testOnBorrow这个参数
进行控制是否启用校验功能,这个过程会消耗一定的性能,在测试阶段可以使用,在线上不建议开启。
高并发下线程池的使用
JDK1.5中引入了ThreadPoolExcutor,ThreadPoolExcutor中包含了两个重要的参数:coreThreadSize和maxThreadSize,这两个参数控制着线程池的执行过程。线程池执行过程如下图所示:
线程池执行流程
1、如果线程池中的线程数少于coreThreadSize时,处理任务就会创建新的线程
2、如果线程池中的线程数大于coreThreadSize时,将任务丢到一个任务队列里,由空闲线程执行
3、如果任务队列放满时,则继续创建线程,直到maxThreadSize
4、当达到maxThreadSize时,还在继续提交任务时,就不得不舍弃任务
在线程池使用的过程中有很多地方都是值得我们考量的地方:
//CPU密集型线程池的考量
JDK实现的这个线程池,优先将任务暂存到队列中,而不是执行任务,它比较适合CPU密集型任务,
也就是需要进行大量CPU计算的任务。执行CPU密集型任务时CPU比较繁忙,只需创建和CPU核心数
相同的线程数,多了反而会造成线程上下文切换,反而会影响性能。所以当任务超过coreThreadSize时,
线程池不会创建线程,而是放到任务队列中,等待核心线程来处理。
//IO密集型线程池的考量
我们平时开发的web项目中有大量的IO密集型任务处理,比如数据库查询,缓存查询等等。IO任务执行
时CPU就停下来了,如果,此时,增加线程数而不是放到队列里,单位时间内就可以执行更多的任务
大大提高任务执行的吞吐量。比如Tomcat中使用的线程池就是在JDK原生的线程池中进行改造,当线程
数超过coreThreadSize时,就会创建线程,知道线程数达到maxThreadSize,这样的就比较适合web系统
的大量IO操作。
//任务队列的考量
1、线程池中使用的任务队列也是我们考量的一个重要指标,首先任务队列必须时有界队列,否则,任务
持续堆积会导致jvm执行full gc 最终服务不可用问题,或者产生内存的泄露问题。
2、任务队列必须要与coreThreadSize和maxThreadSize配合使用,如果coreThreadSize和maxThreadSize
设置过小,会导致任务丢给线程池,长时间得不到执行的诡异问题
连接池和线程池的共同点
- 他们所管理的对象无论时连接还是线程,在创建时都需要消耗系统资源。把它们都放到一个池中统一管理,以便提升性能和资源复用。
- 池化技术的核心思想:空间换时间。使用预先创建好的对象避免频繁创建对象带来的开销,同时还可以对对象统一管理,降低对象的使用成本。
- 在创建对象时,也要存在内存占用这一缺点,但是这一缺点相对池化技术的优势是可以忽略的。
总结
- 无论是连接池还是线程池,他们的最大值和最小值都很重要。初期可以根据经验来设置,后期要根据业务来做调整
- 池子在使用的时候必须进行预热。比如,在使用线程池的时候,先要初始化所有的核心线程,如果池子不进行预热的话,可能会在系统重启的时候导致比较多的慢请求。
- 池化技术是一种空间换时间的思想,所以在使用池化技术的时候要注意内存的占用,在过度使用内存的时候要避免内存泄漏和频繁的full gc。
最后找了一张连接池对比关系图送给大家:
未完待续