高并发下场景,数据库优化方案(上):池化技术
前情回顾
大家好,我是小龙。
上一篇文章我们说到
今天,我们继续解读牛客面经,往期回顾或则持续追更,订阅【牛客面经解读
】专栏。
通常很多大厂面试都会涉及高并发相关的问题,接下来几期,想和大家就MySQL数据库聊聊它的优化方案。
我先考考你:如何减少频繁创建数据库连接损耗的性能?数据库查询请求增加时怎样优化?写入数据增加时,又该怎样应对?
话不多说,正文开始~
池化技术
当你的系统访问量增加,系统访问开始变慢时。这时你尝试查看程序日志,发现系统变慢主要是因为和数据库频繁交互上。
简单回顾一下程序和数据库交互过程:
-
获取数据库连接 -
依靠获得的连接进行数据库相关操作 -
使用完后将数据库连接资源释放
如果对以上知识不熟的同学可以回顾一下JDBC系列基础知识。
按照上述方式,若是你每次需要执行一次SQL,你都得获取一次连接
,然后执行,马上又释放,请求少可能不觉得,但是若请求量上来,务必导致数据库连接的不断创建与释放,很大程度的浪费了时间,频繁地建立数据库连接耗费时间长导致了访问慢
的问题。
此时,心想,若可以让数据库连接复用,那不是可以大大减少频繁创建数据库连接的性能损耗。
此时,便引进了数据库的池化技术
。
池化技术
的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用成本。用连接池预先建立数据库连接。
开发中,会有很多连接池:
-
数据库连接池 -
HTTP连接池 -
Redis连接池
而连接池的管理是连接池设计的核心,我就以数据库连接池为例,来说明一下连接池管理的关键点。
数据库连接池
两个最重要的配置:最小连接数、最大连接数
流程
-
如果当前连接数小于最小连接数,则创建新的连接数处理数据库请求 -
如果连接池中有控件连接则复用空闲连接 -
如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求 -
如果当前连接数大于等于最大连接数,则按照配置中设定的时间等等旧的连接可用 -
如果超过了这个设定的时间,则向外抛出错误
线上设置多少合适
-
对于数据库连接池的设置,线上最小连接数控制在10左右,最大连接数控制在20-30左右即可。 -
启动一个线程来定期检测连接池中的连接是否可用,SELECT 1 FROM DUAL -
testOnBorrow 设置为 false
需要注意的是池子中连接的维护问题
通常我们可以:
1、启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送 “select 1
” 的命令给数据库,看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用
2、在获取到连接之后,先校验连接是否可用,如果可用才会执行 SQL 语句。比如 DBCP 连接池的 testOnBorrow 配置项,就是控制是否开启这个验证。这种方式在获取连接时会引入多余的开销,在线上系统中还是尽量不要开启,在测试服务上可以使用。
至此,我们简单梳理了连接池的工作原理。
但是,假如我们遇到需要多次访问数据库的场景时,可能又会出现新的瓶颈。
此刻,若是能够利用并发思想,创建多个线程处理与数据库之间的交互,这样性能便又可以提升一大截。
有了之前的经验,数据库连接可以用池化来提高性能,多线程依旧可以利用池化(线程池)来提高性能,减少开销,更方便线程间管理。
这里对线程池做个简单回顾:
线程池
一般推荐使用 ThreadPoolExecutor 创建线程池。
《阿⾥巴巴 Java 开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过 ThreadPoolExecutor 的⽅式,避免使用 Executors创建线程池,主要是避免使用其中的默认实现(比如定长缓存池使用链表任务队列,默认长度为 Integer.MAX_VALUE;
可能堆积大量请求,导致 OOM
)那么我们可以自己直接调用 ThreadPoolExecutor 的构造函数自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。规避资源耗尽的⻛险。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
各个参数说明:
-
corePoolSize:线程池中的常驻核心线程数 -
maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1 -
keepAliveTime:多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁,直到只剩下corePoolSize个线程为止 -
unit:keepAliveTime的单位 -
workQueue:任务队列,被提交但尚未被执行的任务 -
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可 -
Handler:拒绝策略,表示当队列满了,并且工作线程大于线程池的最大线程数时如何来拒绝请求执行的runnable的策略
小龙提醒:
优先将任务放入队列中暂存起来,而不是创建更多的线程
JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务;
why?
因为执行 CPU 密集型的任务时 CPU 比较繁忙
,因此只需要创建和 CPU 核数相当的线程就好了,多了反而会造成线程上下文切换
,降低任务执行效率。所以当当前线程数超过核心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来
但是,我们平时开发的 Web 系统通常都有大量的 IO 操作,比方说查询数据库、查询缓存
等等。任务在执行 IO 操作的时候 CPU 就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列
中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量
线程池中使用的队列的堆积量也是我们需要监控的重要指标
若线程池的 coreThreadCount 和 maxThreadCount 设置得比较小,可能导致任务在线程池里面大量的堆积,进而出现任务丢给线程池之后,长时间都不会被执行的问题。
使用线程池请一定记住不要使用无界队列(即没有设置固定大小的队列)
大量的任务堆积会占用大量的内存空间,一旦内存空间被占满就会频繁地触发 Full GC,造成服务不可用。
写在最后
本期到此结束。