通过源码告诉你阿里的数据库连接池Druid有多牛

简介

druid是用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能,另外,druid还扩展了监控统计、防御SQL注入等功能。

使用例子-入门

需求

使用druid连接池获取连接对象,对用户数据进行简单的增删改查(sql脚本项目中已提供)。

工程环境

JDK:1.8.0_231

maven:3.6.1

IDE:eclipse 4.12

mysql-connector-java:8.0.15

mysql:5.7 .28

druid:1.1.20

主要步骤
  1. 编写druid.properties,设置数据库连接参数和连接池基本参数等
  2. 通过DruidDataSourceFactory加载druid.properties文件,并创建DruidDataSource对象
  3. 通过DruidDataSource对象获得Connection对象
  4. 使用Connection对象对用户表进行增删改查
创建项目

项目类型Maven Project,打包方式war(其实jar也可以,之所以使用war是为了测试JNDI)。

引入依赖

这里引入日志包,主要为了看看连接池的创建过程,不引入不会有影响的。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

编写druid.properties

配置文件路径在resources目录下,因为是入门例子,这里仅给出数据库连接参数和连接池基本参数,后面会对所有配置参数进行详细说明。另外,数据库sql脚本也在该目录下。

当然,我们也可以通过启动参数来进行配置(但这种方式可配置参数会少一些)。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

获取连接池和获取连接

项目中编写了JDBCUtil来初始化连接池、获取连接、管理事务和释放资源等,具体参见项目源码。

路径:cn.zzs.druid

通过源码告诉你阿里的数据库连接池Druid有多牛

 

编写测试类

这里以保存用户为例,路径在test目录下的cn.zzs.druid。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

使用例子-通过JNDI获取数据源 需求

本文测试使用JNDI获取DruidDataSource对象,选择使用tomcat 9.0.21作容器。

如果之前没有接触过JNDI,并不会影响下面例子的理解,其实可以理解为像spring的bean配置和获取。

引入依赖

本文在入门例子的基础上增加以下依赖,因为是web项目,所以打包方式为war:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

编写context.xml

在webapp文件下创建目录META-INF,并创建context.xml文件。这里面的每个resource节点都是我们配置的对象,类似于spring的bean节点。其中jdbc/druid-test可以看成是这个bean的id。

注意,这里获取的数据源对象是单例的,如果希望多例,可以设置singleton="false"。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

编写web.xml

在web-app节点下配置资源引用,每个resource-ref指向了我们配置好的对象。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

编写jsp

因为需要在web环境中使用,如果直接建议写个main方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp来测试吧。

druid提供了DruidDataSourceFactory来支持JNDI。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

测试结果

打包项目在tomcat9上运行,访问 http://localhost:8080/druid-demo/testJNDI.jsp ,控制台打印如下内容:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

使用例子-开启监控统计

在以上例子基础上修改。

配置StatFilter 打开监控统计功能

druid的监控统计功能是通过filter-chain扩展实现,如果你要打开监控统计功能,配置StatFilter,如下:

filters=stat

stat是com.alibaba.druid.filter.stat.StatFilter的别名,别名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties。

SQL合并配置

当你程序中存在没有参数化的sql执行时,sql统计的效果会不好。比如:

select * from t where id = 1
select * from t where id = 2
select * from t where id = 3

在统计中,显示为3条sql,这不是我们希望要的效果。StatFilter提供合并的功能,能够将这3个SQL合并为如下的SQL:

select * from t where id = ?

可以配置StatFilter的mergeSql属性来解决:

#用于设置filter的属性
#多个参数用";"隔开
connectionProperties=druid.stat.mergeSql=true

StatFilter支持一种简化配置方式,和上面的配置等同的。如下:

filters=mergeStat

mergeStat是的MergeStatFilter缩写,我们看MergeStatFilter的实现:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

从实现代码来看,仅仅是一个mergeSql的缺省值。

慢SQL记录

StatFilter属性slowSqlMillis用来配置SQL慢的标准,执行时间超过slowSqlMillis的就是慢。slowSqlMillis的缺省值为3000,也就是3秒。

connectionProperties=druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000

在上面的配置中,slowSqlMillis被修改为5秒,并且通过日志输出执行慢的SQL。

合并多个DruidDataSource的监控数据

缺省多个DruidDataSource的监控数据是各自独立的,在druid-0.2.17版本之后,支持配置公用监控数据,配置参数为useGlobalDataSourceStat。例如:

connectionProperties=druid.useGlobalDataSourceStat=true
配置StatViewServlet

druid内置提供了一个StatViewServlet用于展示Druid的统计信息。

这个StatViewServlet的用途包括:

  • 提供监控信息展示的html页面
  • 提供监控信息的JSON API

注意:使用StatViewServlet,建议使用druid 0.2.6以上版本。

配置web.xml

StatViewServlet是一个标准的javax.servlet.http.HttpServlet,需要配置在你web应用中的WEB-INF/web.xml中。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

根据配置中的url-pattern来访问内置监控页面,如果是上面的配置,内置监控页面的首页是/druid/index.html

配置监控页面访问密码

需要配置Servlet的 loginUsername 和 loginPassword这两个初始参数。

示例如下:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

配置allow和deny

StatViewSerlvet展示出来的监控信息比较敏感,是系统运行的内部情况,如果你需要做访问控制,可以配置allow和deny这两个参数。比如:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

判断规则:

  1. deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝。
  2. 如果allow没有配置或者为空,则允许所有访问
配置resetEnable

在StatViewSerlvet输出的html页面中,有一个功能是Reset All,执行这个操作之后,会导致所有计数器清零,重新计数。你可以通过配置参数关闭它。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

配置WebStatFilter

WebStatFilter用于采集web-jdbc关联监控的数据。经常需要排除一些不必要的url,比如.js,/jslib/等等。配置在init-param中。比如:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

测试

启动程度,访问http://localhost:8080/druid-demo/druid/index.html,登录后可见以下页面,通过该页面我们可以查看数据源配置参数、进行SQL统计和监控,等等:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

druid监控统计页面

使用例子-防御SQL注入 开启WallFilter

WallFilter用于对SQL进行拦截,通过以下配置开启:

#过滤器
filters=wall,stat

注意,这种配置拦截检测的时间不在StatFilter统计的SQL执行时间内。 如果希望StatFilter统计的SQL执行时间内,则使用如下配置

#过滤器
filters=stat,wall
WallConfig详细说明

WallFilter常用参数如下,可以通过connectionProperties属性进行配置:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

使用例子-日志记录JDBC执行的SQL

开启日志记录

druid内置提供了四种LogFilter(Log4jFilter、Log4j2Filter、CommonsLogFilter、Slf4jLogFilter),用于输出JDBC执行的日志。这些Filter都是Filter-Chain扩展机制中的Filter,所以配置方式可以参考这里:

#过滤器
filters=log4j

在druid-xxx.jar!/META-INF/druid-filter.properties文件中描述了这四种Filter的别名:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

他们的别名分别是log4j、log4j2、slf4j、commonlogging和commonLogging。其中commonlogging和commonLogging只是大小写不同。

配置输出日志

缺省输入的日志信息全面,但是内容比较多,有时候我们需要定制化配置日志输出。

connectionProperties=druid.log.rs=false

相关参数如下,更多参数请参考com.alibaba.druid.filter.logging.LogFilter:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

log4j.properties配置

如果你使用log4j,可以通过log4j.properties文件配置日志输出选项,例如:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

输出可执行的SQL

参数配置方式

connectionProperties=druid.log.stmt.executableSql=true
配置文件详解 配置druid的参数的n种方式

使用druid,同一个参数,我们可以采用多种方式进行配置,举个例子:maxActive(最大连接池参数)的配置:

方式一(系统属性)

系统属性一般在启动参数中设置。通过方式一来配置连接池参数的还是比较少见。

-Ddruid.maxActive=8
方式二(properties)

这是最常见的一种。

maxActive=8
方式三(properties加前缀)

相比第二种方式,这里只是加了.druid前缀。

druid.maxActive=8
方式四(properties的connectionProperties)

connectionProperties可以用于配置多个属性,不同属性使用";"隔开。

connectionProperties=druid.maxActive=8
方式五(connectProperties)

connectProperties可以在方式一、方式三和方式四中存在,具体配置如下:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

这个属性甚至可以这样配(当然应该没人会这么做):

druid.connectProperties=druid.connectProperties=druid.connectProperties=druid.connectProperties=druid.maxActive=8

真的是没完没了,怎么会引入connectProperties这个属性呢?我觉得这是一个十分失败的设计,所以本文仅会讲前面说的四种。

关于druid参数配置的吐槽

前面已经讲到,同一个参数,我们有时可以采用无数种方式来配置。表面上看这样设计十分人性化,可以适应不同人群的使用习惯,但是,在我看来,这样设计非常不利于配置的统一管理,另外,druid的参数配置还存在另一个问题,先看下这个表格(这里包含了druid所有的参数,使用时可以参考):

通过源码告诉你阿里的数据库连接池Druid有多牛

 

一般我们都希望采用一种方式来统一配置这些参数,但是,通过以上表格可知,druid并不存在哪一种方式能配置所有参数,也就是说,你不得不采用两种或两种以上的配置方式。所以,我认为,至少在配置方式这一点上,druid是非常失败的!

通过表格可知,方式二和方式四结合使用,可以覆盖所有参数,所以,本文采用的配置策略为:优先采用方式二配置,配不了再选用方式四。

数据库连接参数

注意,这里在url后面拼接了多个参数用于避免乱码、时区报错问题。 补充下,如果不想加入时区的参数,可以在mysql命令窗口执行如下命令:set global time_zone='+8:00'。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

连接池数据基本参数

这几个参数都比较常用,具体设置多少需根据项目调整。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

连接检查参数

针对连接失效的问题,建议开启空闲连接测试,而不建议开启借出测试(从性能考虑),另外,开启连接测试时,必须配置validationQuery。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

缓存语句

针对大部分数据库而言,开启缓存语句可以有效提高性能,但是在myslq下建议关闭。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

事务相关参数

建议保留默认就行。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

连接泄漏回收参数

通过源码告诉你阿里的数据库连接池Druid有多牛

 

过滤器

通过源码告诉你阿里的数据库连接池Druid有多牛

 

其他

通过源码告诉你阿里的数据库连接池Druid有多牛

 

源码分析

看过druid的源码就会发现,相比其他DBCP和C3P0,druid有以下特点:

  1. 更多地引入了JDK的特性,特别是concurrent包的工具。例如,CountDownLatch、ReentrantLock、AtomicLongFieldUpdater、Condition等,也就是说,在分析druid源码之前,最好先学习下这些技术;
  2. 在类的设计上一切从简。例如,DBCP和C3P0都有一个池的类,而druid并没有,只用了一个简单的数组,且druid的核心逻辑几乎都堆积在DruidDataSource里面。另外,在对类或接口的抽象上,个人感觉,druid不是很“面向对象”,有的接口或类的方法很难统一成某种对象的行为,所以,本文不会去关注类的设计,更多地将分析一些重要功能的实现。

注意:考虑篇幅和可读性,以下代码经过删减,仅保留所需部分。

配置参数的加载

前面已经讲过,druid为我们提供了“无数”种方式来配置参数,这里我再补充下不同配置方式的加载顺序(当然,只会涉及到四种方式)。

当我们使用调用DruidDataSourceFactory.createDataSource(Properties)时,会加载配置来给对应的属性赋值,另外,这个过程还会根据配置去创建对应的过滤器。不同配置方式加载时机不同,后者会覆盖已存在的相同参数,如图所示。

通过源码告诉你阿里的数据库连接池Druid有多牛

druid不同配置方式的加载顺序

数据源的初始化 了解下DruidDataSource这个类

这里先来介绍下DruidDataSource这个类:

通过源码告诉你阿里的数据库连接池Druid有多牛

DruidDataSource的UML图

图中我只列出了几个重要的属性,这几个属性没有理解好,后面的源码很难看得进去。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

概括下初始化的过程

DruidDataSource的初始化时机是可选的,当我们设置init=true时,在createDataSource时就会调用DataSource.init()方法进行初始化,否则,只会在getConnection时再进行初始化。数据源初始化主要逻辑在DataSource.init()这个方法,可以概括为以下步骤:

  1. 加锁
  2. 初始化initStackTrace、id、xxIdSeed、dbTyp、driver、dataSourceStat、connections、evictConnections、keepAliveConnections等属性
  3. 初始化过滤器
  4. 校验maxActive、minIdle、initialSize、timeBetweenLogStatsMillis、useGlobalDataSourceStat、maxEvictableIdleTimeMillis、minEvictableIdleTimeMillis、validationQuery等配置是否合法
  5. 初始化ExceptionSorter、ValidConnectionChecker、JdbcDataSourceStat
  6. 创建initialSize数量的连接
  7. 创建logStatsThread、createConnectionThread和destroyConnectionThread
  8. 等待createConnectionThread和destroyConnectionThread线程run后再继续执行
  9. 注册MBean,用于支持JMX
  10. 如果设置了keepAlive,通知createConnectionThread创建连接对象
  11. 解锁

这个方法差不多200行,考虑篇幅,我删减了部分内容。

加锁和解锁

druid数据源初始化采用的是ReentrantLock,如下:

通过源码告诉你阿里的数据库连接池Druid有多牛

 

注意,以下步骤均在这个锁的范围内。

初始化属性

这部分内容主要是初始化一些属性,需要注意的一点就是,这里使用了AtomicLongFieldUpdater来进行原子更新,保证写的安全和读的高效,当然,还是cocurrent包的工具。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

初始化过滤器

看到下面的代码会发现,我们还可以通过SPI机制来配置过滤器。

使用SPI配置过滤器时需要注意,对应的类需要加上@AutoLoad注解,另外还需要配置load.spifilter.skip=false,SPI相关内容可参考我的另一篇博客:使用SPI解耦你的实现类。

在这个方法里,主要就是初始化过滤器的一些属性而已。过滤器的部分,本文不会涉及到太多。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

校验配置

这里只是简单的校验,不涉及太多复杂的逻辑。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

初始化ExceptionSorter、ValidConnectionChecker、JdbcDataSourceStat

这里重点关注ExceptionSorter和ValidConnectionChecker这两个类,这里会根据数据库类型进行选择。其中,ValidConnectionChecker用于对连接进行检测。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

创建initialSize数量的连接

这里有两种方式创建连接,一种是异步,一种是同步。但是,根据我们的使用例子,createScheduler为null,所以采用的是同步的方式。

注意,后面的所有代码也是基于createScheduler为null来分析的。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

创建logStatsThread、createConnectionThread和destroyConnectionThread

这里会启动三个线程。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

等待

这里使用了CountDownLatch,保证当createConnectionThread和destroyConnectionThread开始run时再继续执行。

        private final CountDownLatch initedLatch = new CountDownLatch(2);
        // 线程进入等待,等待CreatorThread和DestroyThread执行
        initedLatch.await();

我们进入到DruidDataSource.CreateConnectionThread.run(),可以看到,一执行run方法就会调用countDown。destroyConnectionThread也是一样,这里就不放进来了。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

注册MBean

接下来是注册MBean,会去注册DruidDataSourceStatManager和DruidDataSource,启动我们的程度,通过jconsole就可以看到这两个MBean。JMX相关内容这里就不多扩展了,感兴趣的话可参考我的另一篇博客: 如何使用JMX来管理程序?

        // 注册MBean,用于支持JMX
        registerMbean();
通知createConnectionThread创建连接对象

前面已经讲过,当我们调用empty.signal(),会去唤醒处于empty.await()状态的CreateConnectionThread。CreateConnectionThread这个线只有在需要创建连接时才运行,否则会一直等待,后面会讲到。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

连接对象的获取

了解下DruidPooledConnection这个类

用户调用DruidDataSource.getConnection,拿到的对象是DruidPooledConnection,里面封装了DruidConnectionHolder,而这个对象包含了原生的连接对象和我们一开始创建的数据源对象。

通过源码告诉你阿里的数据库连接池Druid有多牛

DruidPooledConnection的UML图

概括下获取连接的过程

连接对象的获取过程可以概括为以下步骤:

  1. 初始化数据源(如果还没初始化);
  2. 获得连接对象,如果无可用连接,向createConnectionThread发送signal创建新连接,此时会进入等待;
  3. 如果设置了testOnBorrow,进行testOnBorrow检测,否则,如果设置了testWhileIdle,进行testWhileIdle检测;
  4. 如果设置了removeAbandoned,则会将连接对象放入activeConnections;
  5. 设置defaultAutoCommit,并返回;
  6. 执行filterChain。

初始化数据源的前面已经讲过了,这里就直接从第二步开始。

获取连接对象

进入DruidDataSource.getConnectionInternal方法。除了获取连接对象,其他的大部分是校验和计数的内容。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

下面再看下DruidDataSource.takeLast()方法(即没有配置maxWait时调用的方法)。该方法中,当没有空闲连接对象时,会尝试创建连接,此时该线程进入等待(notEmpty.await()),只有连接对象创建完成或池中回收了连接对象(notEmpty.signal()),该线程才会继续执行。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

创建连接对象

前面已经讲到,创建连接是采用异步方式,进入到DruidDataSource.CreateConnectionThread.run()。当不需要创建连接时,该线程进入empty.await()状态,此时需要用户线程调用empty.signal()来唤醒。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

testOnBorrow或testWhileIdle

进入DruidDataSource.getConnectionDirect(long)。该方法会使用到validConnectionChecker来校验连接的有效性。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

removeAbandoned

进入DruidDataSource.getConnectionDirect(long),这里不会进行检测,只是将连接对象放入activeConnections,具体泄露连接的检测工作是在DestroyConnectionThread线程中进行。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

DestroyConnectionThread线程会根据我们设置的timeBetweenEvictionRunsMillis来进行检验,具体的校验会去运行DestroyTask(DruidDataSource的内部类),这里看下DestroyTask的run方法。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

进入DruidDataSource.removeAbandoned(),当连接对象使用时间超过removeAbandonedTimeoutMillis,则会被丢弃掉。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

执行filterChain

进入DruidDataSource.getConnection。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

进入到FilterChainImpl.dataSource_connect。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

这里以StatFilter.dataSource_getConnection为例。

通过源码告诉你阿里的数据库连接池Druid有多牛

 

以上,druid的源码基本已经分析完,其他部分内容有空再做补充。

上一篇:18.Springboot_高级配置 Druid 连接池与监控管理


下一篇:Dapr + .NET Core实战(十一)单机Dapr集群