vlambda博客
学习文章列表

高并发下场景,数据库优化方案(上):池化技术

前情回顾

大家好,我是小龙。

上一篇文章我们说到

今天,我们继续解读牛客面经,往期回顾或则持续追更,订阅【牛客面经解读】专栏。

通常很多大厂面试都会涉及高并发相关的问题,接下来几期,想和大家就MySQL数据库聊聊它的优化方案。

我先考考你:如何减少频繁创建数据库连接损耗的性能?数据库查询请求增加时怎样优化?写入数据增加时,又该怎样应对?

话不多说,正文开始~

池化技术

当你的系统访问量增加,系统访问开始变慢时。这时你尝试查看程序日志,发现系统变慢主要是因为和数据库频繁交互上。

简单回顾一下程序和数据库交互过程:

  1. 获取数据库连接
  2. 依靠获得的连接进行数据库相关操作
  3. 使用完后将数据库连接资源释放

如果对以上知识不熟的同学可以回顾一下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,造成服务不可用。

写在最后

本期到此结束。