简介

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

本文将包含以下内容(因为篇幅较长,可根据需要选择阅读):

  1. druid的使用方法(入门案例、JDNI使用、监控统计、防御SQL注入)
  2. druid的配置参数详解
  3. druid主要源码分析

其他连接池的内容也可以参考我的其他博客:

源码详解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)

源码详解系列(五) ------ C3P0的使用和分析(包括JNDI)

使用例子-入门

需求

使用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)。

引入依赖

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

  1. <dependency>
  2. <groupId>junit</groupId>
  3. <artifactId>junit</artifactId>
  4. <version>4.12</version>
  5. <scope>test</scope>
  6. </dependency>
  7. <!-- druid -->
  8. <dependency>
  9. <groupId>com.alibaba</groupId>
  10. <artifactId>druid</artifactId>
  11. <version>1.1.20</version>
  12. </dependency>
  13. <!-- mysql驱动 -->
  14. <dependency>
  15. <groupId>mysql</groupId>
  16. <artifactId>mysql-connector-java</artifactId>
  17. <version>8.0.15</version>
  18. </dependency>
  19. <!-- log -->
  20. <dependency>
  21. <groupId>log4j</groupId>
  22. <artifactId>log4j</artifactId>
  23. <version>1.2.17</version>
  24. </dependency>
  25. <dependency>
  26. <groupId>commons-logging</groupId>
  27. <artifactId>commons-logging</artifactId>
  28. <version>1.2</version>
  29. </dependency>

编写druid.properties

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

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

  1. #-------------基本属性--------------------------------
  2. url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
  3. username=root
  4. password=root
  5. #数据源名,当配置多数据源时可以用于区分。注意,1.0.5版本及更早版本不支持配置该项
  6. #默认"DataSource-" + System.identityHashCode(this)
  7. name=zzs001
  8. #如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
  9. driverClassName=com.mysql.cj.jdbc.Driver
  10. #-------------连接池大小相关参数--------------------------------
  11. #初始化时建立物理连接的个数
  12. #默认为0
  13. initialSize=0
  14. #最大连接池数量
  15. #默认为8
  16. maxActive=8
  17. #最小空闲连接数量
  18. #默认为0
  19. minIdle=0
  20. #已过期
  21. #maxIdle
  22. #获取连接时最大等待时间,单位毫秒。
  23. #配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
  24. #默认-1,表示无限等待
  25. maxWait=-1

获取连接池和获取连接

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

路径:cn.zzs.druid

  1. Properties properties = new Properties();
  2. InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties");
  3. properties.load(in);
  4. DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);

编写测试类

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

  1. @Test
  2. public void save() throws SQLException {
  3. // 创建sql
  4. String sql = "insert into demo_user values(null,?,?,?,?,?)";
  5. Connection connection = null;
  6. PreparedStatement statement = null;
  7. try {
  8. // 获得连接
  9. connection = JDBCUtils.getConnection();
  10. // 开启事务设置非自动提交
  11. connection.setAutoCommit(false);
  12. // 获得Statement对象
  13. statement = connection.prepareStatement(sql);
  14. // 设置参数
  15. statement.setString(1, "zzf003");
  16. statement.setInt(2, 18);
  17. statement.setDate(3, new Date(System.currentTimeMillis()));
  18. statement.setDate(4, new Date(System.currentTimeMillis()));
  19. statement.setBoolean(5, false);
  20. // 执行
  21. statement.executeUpdate();
  22. // 提交事务
  23. connection.commit();
  24. } finally {
  25. // 释放资源
  26. JDBCUtils.release(connection, statement, null);
  27. }
  28. }

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

需求

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

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

引入依赖

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

  1. <dependency>
  2. <groupId>javax.servlet</groupId>
  3. <artifactId>jstl</artifactId>
  4. <version>1.2</version>
  5. <scope>provided</scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>javax.servlet</groupId>
  9. <artifactId>javax.servlet-api</artifactId>
  10. <version>3.1.0</version>
  11. <scope>provided</scope>
  12. </dependency>
  13. <dependency>
  14. <groupId>javax.servlet.jsp</groupId>
  15. <artifactId>javax.servlet.jsp-api</artifactId>
  16. <version>2.2.1</version>
  17. <scope>provided</scope>
  18. </dependency>

编写context.xml

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

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

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <Context>
  3. <Resource
  4. name="jdbc/druid-test"
  5. factory="com.alibaba.druid.pool.DruidDataSourceFactory"
  6. auth="Container"
  7. type="javax.sql.DataSource"
  8. maxActive="15"
  9. initialSize="3"
  10. minIdle="3"
  11. maxWait="10000"
  12. url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true"
  13. username="root"
  14. password="root"
  15. filters="mergeStat,log4j"
  16. validationQuery="select 1 from dual"
  17. />
  18. </Context>

编写web.xml

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

  1. <!-- JNDI数据源 -->
  2. <resource-ref>
  3. <res-ref-name>jdbc/druid-test</res-ref-name>
  4. <res-type>javax.sql.DataSource</res-type>
  5. <res-auth>Container</res-auth>
  6. </resource-ref>

编写jsp

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

druid提供了DruidDataSourceFactory来支持JNDI

  1. <body>
  2. <%
  3. String jndiName = "java:comp/env/jdbc/druid-test";
  4. InitialContext ic = new InitialContext();
  5. // 获取JNDI上的ComboPooledDataSource
  6. DataSource ds = (DataSource) ic.lookup(jndiName);
  7. JDBCUtils.setDataSource(ds);
  8. // 创建sql
  9. String sql = "select * from demo_user where deleted = false";
  10. Connection connection = null;
  11. PreparedStatement statement = null;
  12. ResultSet resultSet = null;
  13. // 查询用户
  14. try {
  15. // 获得连接
  16. connection = JDBCUtils.getConnection();
  17. // 获得Statement对象
  18. statement = connection.prepareStatement(sql);
  19. // 执行
  20. resultSet = statement.executeQuery();
  21. // 遍历结果集
  22. while(resultSet.next()) {
  23. String name = resultSet.getString(2);
  24. int age = resultSet.getInt(3);
  25. System.err.println("用户名:" + name + ",年龄:" + age);
  26. }
  27. } catch(SQLException e) {
  28. System.err.println("查询用户异常");
  29. } finally {
  30. // 释放资源
  31. JDBCUtils.release(connection, statement, resultSet);
  32. }
  33. %>
  34. </body>

测试结果

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

  1. 用户名:zzs001,年龄:18
  2. 用户名:zzs002,年龄:18
  3. 用户名:zzs003,年龄:25
  4. 用户名:zzf001,年龄:26
  5. 用户名:zzf002,年龄:17
  6. 用户名:zzf003,年龄:18

使用例子-开启监控统计

在以上例子基础上修改。

配置StatFilter

打开监控统计功能

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

  1. filters=stat

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

SQL合并配置

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

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

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

  1. select * from t where id = ?

可以配置StatFiltermergeSql属性来解决:

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

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

  1. filters=mergeStat

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

  1. public class MergeStatFilter extends StatFilter {
  2. public MergeStatFilter() {
  3. super.setMergeSql(true);
  4. }
  5. }

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

慢SQL记录

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

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

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

合并多个DruidDataSource的监控数据

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

  1. 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中。

  1. <servlet>
  2. <servlet-name>DruidStatView</servlet-name>
  3. <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>DruidStatView</servlet-name>
  7. <url-pattern>/druid/*</url-pattern>
  8. </servlet-mapping>

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

例如:

http://localhost:8080/druid-demo/druid/index.html

配置监控页面访问密码

需要配置ServletloginUsername loginPassword这两个初始参数。

示例如下:

  1. <!-- 配置 Druid 监控信息显示页面 -->
  2. <servlet>
  3. <servlet-name>DruidStatView</servlet-name>
  4. <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
  5. <init-param>
  6. <!-- 允许清空统计数据 -->
  7. <param-name>resetEnable</param-name>
  8. <param-value>true</param-value>
  9. </init-param>
  10. <init-param>
  11. <!-- 用户名 -->
  12. <param-name>loginUsername</param-name>
  13. <param-value>druid</param-value>
  14. </init-param>
  15. <init-param>
  16. <!-- 密码 -->
  17. <param-name>loginPassword</param-name>
  18. <param-value>druid</param-value>
  19. </init-param>
  20. </servlet>
  21. <servlet-mapping>
  22. <servlet-name>DruidStatView</servlet-name>
  23. <url-pattern>/druid/*</url-pattern>
  24. </servlet-mapping>

配置allow和deny

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

  1. <servlet>
  2. <servlet-name>DruidStatView</servlet-name>
  3. <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
  4. <init-param>
  5. <param-name>allow</param-name>
  6. <param-value>128.242.127.1/24,128.242.128.1</param-value>
  7. </init-param>
  8. <init-param>
  9. <param-name>deny</param-name>
  10. <param-value>128.242.127.4</param-value>
  11. </init-param>
  12. </servlet>

判断规则:

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

配置resetEnable

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

  1. <servlet>
  2. <servlet-name>DruidStatView</servlet-name>
  3. <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
  4. <init-param>
  5. <param-name>resetEnable</param-name>
  6. <param-value>false</param-value>
  7. </init-param>
  8. </servlet>

配置WebStatFilter

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

  1. <filter>
  2. <filter-name>DruidWebStatFilter</filter-name>
  3. <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
  4. <init-param>
  5. <param-name>exclusions</param-name>
  6. <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
  7. </init-param>
  8. </filter>
  9. <filter-mapping>
  10. <filter-name>DruidWebStatFilter</filter-name>
  11. <url-pattern>/*</url-pattern>
  12. </filter-mapping>

测试

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

使用例子-防御SQL注入

开启WallFilter

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

  1. #过滤器
  2. filters=wall,stat

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

  1. #过滤器
  2. filters=stat,wall

WallConfig详细说明

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

参数 缺省值 描述
wall.logViolation false 对被认为是攻击的SQL进行LOG.error输出
wall.throwException true 对被认为是攻击的SQL抛出SQLException
wall.updateAllow true 是否允许执行UPDATE语句
wall.deleteAllow true 是否允许执行DELETE语句
wall.insertAllow true 是否允许执行INSERT语句
wall.selelctAllow true 否允许执行SELECT语句
wall.multiStatementAllow false 是否允许一次执行多条语句,缺省关闭
wall.selectLimit -1 配置最大返回行数,如果select语句没有指定最大返回行数,会自动修改selct添加返回限制
wall.updateWhereNoneCheck false 检查UPDATE语句是否无where条件,这是有风险的,但不是SQL注入类型的风险
wall.deleteWhereNoneCheck false 检查DELETE语句是否无where条件,这是有风险的,但不是SQL注入类型的风险

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

开启日志记录

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

  1. #过滤器
  2. filters=log4j

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

  1. druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter
  2. druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter
  3. druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter
  4. druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter
  5. druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter

他们的别名分别是log4jlog4j2slf4jcommonloggingcommonLogging。其中commonloggingcommonLogging只是大小写不同。

配置输出日志

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

  1. connectionProperties=druid.log.rs=false

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

参数 说明 properties参数
connectionLogEnabled 所有连接相关的日志 druid.log.conn
statementLogEnabled 所有Statement相关的日志 druid.log.stmt
resultSetLogEnabled 所有ResultSe相关的日志 druid.log.rs
statementExecutableSqlLogEnable 所有Statement执行语句相关的日志 druid.log.stmt.executableSql

log4j.properties配置

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

  1. log4j.logger.druid.sql=warn,stdout
  2. log4j.logger.druid.sql.DataSource=warn,stdout
  3. log4j.logger.druid.sql.Connection=warn,stdout
  4. log4j.logger.druid.sql.Statement=warn,stdout
  5. log4j.logger.druid.sql.ResultSet=warn,stdout

输出可执行的SQL

参数配置方式

  1. connectionProperties=druid.log.stmt.executableSql=true

配置文件详解

配置druid的参数的n种方式

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

方式一(系统属性)

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

  1. -Ddruid.maxActive=8

方式二(properties)

这是最常见的一种。

  1. maxActive=8

方式三(properties加前缀)

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

  1. druid.maxActive=8

方式四(properties的connectionProperties)

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

  1. connectionProperties=druid.maxActive=8

方式五(connectProperties)

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

  1. # 方式一
  2. -Ddruid.connectProperties=druid.maxActive=8
  3. # 方式三:支持多个属性,不同属性使用";"隔开
  4. druid.connectProperties=druid.maxActive=8
  5. # 方式四
  6. connectionProperties=druid.connectProperties=druid.maxActive=8

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

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

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

关于druid参数配置的吐槽

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

参数分类 参数 方式一 方式二 方式三 方式四
基本属性 driverClassName O O O O
password O O O O
url O O O O
username O O O O
事务相关 defaultAutoCommit X O X X
defaultReadOnly X O X X
defaultTransactionIsolation X O X X
defaultCatalog X O X X
连接池大小 maxActive O O O O
maxIdle X O X X
minIdle O O O O
initialSize O O O O
maxWait O O O O
连接检测 testOnBorrow O O O O
testOnReturn X O X X
timeBetweenEvictionRunsMillis O O O O
numTestsPerEvictionRun X O X X
minEvictableIdleTimeMillis O O O O
maxEvictableIdleTimeMillis O X O O
phyTimeoutMillis O O O O
testWhileIdle O O O O
validationQuery O O O O
validationQueryTimeout X O X X
连接泄露回收 removeAbandoned X O X X
removeAbandonedTimeout X O X X
logAbandoned X O X X
缓存语句 poolPreparedStatements O O O O
maxOpenPreparedStatements X O X X
maxPoolPreparedStatementPerConnectionSize O X O O
其他 initConnectionSqls O O O O
init X O X X
asyncInit O X O O
initVariants O X O O
initGlobalVariants O X O O
accessToUnderlyingConnectionAllowed X O X X
exceptionSorter X O X X
exception-sorter-class-name X O X X
name O X O O
notFullTimeoutRetryCount O X O O
maxWaitThreadCount O X O O
failFast O X O O
phyMaxUseCount O X O O
keepAlive O X O O
keepAliveBetweenTimeMillis O X O O
useUnfairLock O X O O
killWhenSocketReadTimeout O X O O
load.spifilter.skip O X O O
cacheServerConfiguration X X X O
过滤器 filters O O O O
clearFiltersEnable O X O O
log.conn O X X O
log.stmt O X X O
log.rs O X X O
log.stmt.executableSql O X X O
timeBetweenLogStatsMillis O X O O
useGlobalDataSourceStat/useGloalDataSourceStat O X O O
resetStatEnable O X O O
stat.sql.MaxSize O X O O
stat.mergeSql O X X O
stat.slowSqlMillis O X X O
stat.logSlowSql O X X O
stat.loggerName X X X O
wall.logViolation O X X O
wall.throwException O X X O
wall.tenantColumn O X X O
wall.updateAllow O X X O
wall.deleteAllow O X X O
wall.insertAllow O X X O
wall.selelctAllow O X X O
wall.multiStatementAllow O X X O
wall.selectLimit O X X O
wall.updateCheckColumns O X X O
wall.updateWhereNoneCheck O X X O
wall.deleteWhereNoneCheck O X X O

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

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

数据库连接参数

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

  1. #-------------基本属性--------------------------------
  2. url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
  3. username=root
  4. password=root
  5. #数据源名,当配置多数据源时可以用于区分。注意,1.0.5版本及更早版本不支持配置该项
  6. #默认"DataSource-" + System.identityHashCode(this)
  7. name=zzs001
  8. #如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
  9. driverClassName=com.mysql.cj.jdbc.Driver

连接池数据基本参数

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

  1. #-------------连接池大小相关参数--------------------------------
  2. #初始化时建立物理连接的个数
  3. #默认为0
  4. initialSize=0
  5. #最大连接池数量
  6. #默认为8
  7. maxActive=8
  8. #最小空闲连接数量
  9. #默认为0
  10. minIdle=0
  11. #已过期
  12. #maxIdle
  13. #获取连接时最大等待时间,单位毫秒。
  14. #配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
  15. #默认-1,表示无限等待
  16. maxWait=-1

连接检查参数

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

  1. #-------------连接检测情况--------------------------------
  2. #用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。
  3. #如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
  4. #默认为空
  5. validationQuery=select 1 from dual
  6. #检测连接是否有效的超时时间,单位:秒。
  7. #底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
  8. #默认-1
  9. validationQueryTimeout=-1
  10. #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
  11. #默认为false
  12. testOnBorrow=false
  13. #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
  14. #默认为false
  15. testOnReturn=false
  16. #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
  17. #建议配置为true,不影响性能,并且保证安全性。
  18. #默认为true
  19. testWhileIdle=true
  20. #有两个含义:
  21. #1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
  22. #2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
  23. #默认1000*60
  24. timeBetweenEvictionRunsMillis=-1
  25. #不再使用,一个DruidDataSource只支持一个EvictionRun
  26. #numTestsPerEvictionRun=3
  27. #连接保持空闲而不被驱逐的最小时间。
  28. #默认值1000*60*30 = 30分钟
  29. minEvictableIdleTimeMillis=1800000

缓存语句

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

  1. #-------------缓存语句--------------------------------
  2. #是否缓存preparedStatement,也就是PSCache。
  3. #PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭
  4. #默认为false
  5. poolPreparedStatements=false
  6. #PSCache的最大个数。
  7. #要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
  8. #在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
  9. #默认为10
  10. maxOpenPreparedStatements=10

事务相关参数

建议保留默认就行。

  1. #-------------事务相关的属性--------------------------------
  2. #连接池创建的连接的默认的auto-commit状态
  3. #默认为空,由驱动决定
  4. defaultAutoCommit=true
  5. #连接池创建的连接的默认的read-only状态。
  6. #默认值为空,由驱动决定
  7. defaultReadOnly=false
  8. #连接池创建的连接的默认的TransactionIsolation状态
  9. #可用值为下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
  10. #默认值为空,由驱动决定
  11. defaultTransactionIsolation=REPEATABLE_READ
  12. #连接池创建的连接的默认的数据库名
  13. defaultCatalog=github_demo

连接泄漏回收参数

  1. #-------------连接泄漏回收参数--------------------------------
  2. #当未使用的时间超过removeAbandonedTimeout时,是否视该连接为泄露连接并删除
  3. #默认为false
  4. removeAbandoned=false
  5. #泄露的连接可以被删除的超时值, 单位毫秒
  6. #默认为300*1000
  7. removeAbandonedTimeoutMillis=300*1000
  8. #标记当Statement或连接被泄露时是否打印程序的stack traces日志。
  9. #默认为false
  10. logAbandoned=true
  11. #连接最大存活时间
  12. #默认-1
  13. #phyTimeoutMillis=-1

过滤器

  1. #-------------过滤器--------------------------------
  2. #属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
  3. #别名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties
  4. #监控统计用的filter:stat(mergeStat可以合并sql)
  5. #日志用的filter:log4j
  6. #防御sql注入的filter:wall
  7. filters=log4j,wall,mergeStat
  8. #用于设置filter、exceptionSorter、validConnectionChecker等的属性
  9. #多个参数用";"隔开
  10. connectionProperties=druid.useGlobalDataSourceStat=true;druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000

其他

  1. #-------------其他--------------------------------
  2. #控制PoolGuard是否容许获取底层连接
  3. #默认为false
  4. accessToUnderlyingConnectionAllowed=false
  5. #当数据库抛出一些不可恢复的异常时,抛弃连接
  6. #根据dbType自动识别
  7. #exceptionSorter
  8. #exception-sorter-class-name=
  9. #物理连接初始化的时候执行的sql
  10. #initConnectionSqls=
  11. #是否创建数据源时就初始化连接池
  12. init=true

源码分析

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

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

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

配置参数的加载

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

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

数据源的初始化

了解下DruidDataSource这个类

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

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

类名 描述
ExceptionSorter 用于判断SQLException对象是否致命异常
ValidConnectionChecker 用于校验指定连接对象是否有效
CreateConnectionThread DruidDataSource的内部类,用于异步创建连接对象
notEmpty 调用notEmpty.await()时,当前线程进入等待;当连接创建完成或者回收了连接,会调用notEmpty.signal()时,将等待线程唤醒;
empty 调用empty.await()时,CreateConnectionThread进入等待;调用empty.signal()时,CreateConnectionThread被唤醒,并进入创建连接;
DestroyConnectionThread DruidDataSource的内部类,用于异步检验连接对象,包括校验空闲连接的phyTimeoutMillis、minEvictableIdleTimeMillis,以及校验借出连接的removeAbandonedTimeoutMillis
LogStatsThread DruidDataSource的内部类,用于异步记录统计信息
connections 用于存放所有连接对象
evictConnections 用于存放需要丢弃的连接对象
keepAliveConnections 用于存放需要keepAlive的连接对象
activeConnections 用于存放需要进行removeAbandoned的连接对象
poolingCount 空闲连接对象的数量
activeCount 借出连接对象的数量

概括下初始化的过程

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

  1. 加锁
  2. 初始化initStackTraceidxxIdSeeddbTypdriverdataSourceStatconnectionsevictConnectionskeepAliveConnections等属性
  3. 初始化过滤器
  4. 校验maxActiveminIdleinitialSizetimeBetweenLogStatsMillisuseGlobalDataSourceStatmaxEvictableIdleTimeMillisminEvictableIdleTimeMillisvalidationQuery等配置是否合法
  5. 初始化ExceptionSorterValidConnectionCheckerJdbcDataSourceStat
  6. 创建initialSize数量的连接
  7. 创建logStatsThreadcreateConnectionThreaddestroyConnectionThread
  8. 等待createConnectionThreaddestroyConnectionThread线程run后再继续执行
  9. 注册MBean,用于支持JMX
  10. 如果设置了keepAlive,通知createConnectionThread创建连接对象
  11. 解锁

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

加锁和解锁

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

  1. final ReentrantLock lock = this.lock;
  2. try {
  3. // 加锁
  4. lock.lockInterruptibly();
  5. } catch (InterruptedException e) {
  6. throw new SQLException("interrupt", e);
  7. }
  8. boolean init = false;
  9. try {
  10. // do something
  11. } finally {
  12. inited = true;
  13. // 解锁
  14. lock.unlock();
  15. }

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

初始化属性

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

  1. // 这里使用了AtomicLongFieldUpdater来进行原子更新,保证了写的安全和读的高效
  2. this.id = DruidDriver.createDataSourceId();
  3. if (this.id > 1) {
  4. long delta = (this.id - 1) * 100000;
  5. this.connectionIdSeedUpdater.addAndGet(this, delta);
  6. this.statementIdSeedUpdater.addAndGet(this, delta);
  7. this.resultSetIdSeedUpdater.addAndGet(this, delta);
  8. this.transactionIdSeedUpdater.addAndGet(this, delta);
  9. }
  10. // 设置url
  11. if (this.jdbcUrl != null) {
  12. this.jdbcUrl = this.jdbcUrl.trim();
  13. // 针对druid自定义的一种url格式,进行解析
  14. // jdbc:wrap-jdbc:开头,可设置driver、name、jmx等
  15. initFromWrapDriverUrl();
  16. }
  17. // 根据url前缀,确定dbType
  18. if (this.dbType == null || this.dbType.length() == 0) {
  19. this.dbType = JdbcUtils.getDbType(jdbcUrl, null);
  20. }
  21. // cacheServerConfiguration,暂时不知道这个参数干嘛用的
  22. if (JdbcConstants.MYSQL.equals(this.dbType)
  23. || JdbcConstants.MARIADB.equals(this.dbType)
  24. || JdbcConstants.ALIYUN_ADS.equals(this.dbType)) {
  25. boolean cacheServerConfigurationSet = false;
  26. if (this.connectProperties.containsKey("cacheServerConfiguration")) {
  27. cacheServerConfigurationSet = true;
  28. } else if (this.jdbcUrl.indexOf("cacheServerConfiguration") != -1) {
  29. cacheServerConfigurationSet = true;
  30. }
  31. if (cacheServerConfigurationSet) {
  32. this.connectProperties.put("cacheServerConfiguration", "true");
  33. }
  34. }
  35. // 设置驱动类
  36. if (this.driverClass != null) {
  37. this.driverClass = driverClass.trim();
  38. }
  39. // 如果我们没有配置driverClass
  40. if (this.driver == null) {
  41. // 根据url识别对应的driverClass
  42. if (this.driverClass == null || this.driverClass.isEmpty()) {
  43. this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
  44. }
  45. // MockDriver的情况,这里不讨论
  46. if (MockDriver.class.getName().equals(driverClass)) {
  47. driver = MockDriver.instance;
  48. } else {
  49. if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) {
  50. throw new SQLException("url not set");
  51. }
  52. // 创建Driver实例,注意,这个过程不需要依赖DriverManager
  53. driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
  54. }
  55. } else {
  56. if (this.driverClass == null) {
  57. this.driverClass = driver.getClass().getName();
  58. }
  59. }
  60. // 用于存放所有连接对象
  61. connections = new DruidConnectionHolder[maxActive];
  62. // 用于存放需要丢弃的连接对象
  63. evictConnections = new DruidConnectionHolder[maxActive];
  64. // 用于存放需要keepAlive的连接对象
  65. keepAliveConnections = new DruidConnectionHolder[maxActive];

初始化过滤器

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

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

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

  1. // 初始化filters
  2. for (Filter filter : filters) {
  3. filter.init(this);
  4. }
  5. // 采用SPI机制加载过滤器,这部分过滤器除了放入filters,还会放入autoFilters
  6. initFromSPIServiceLoader();

校验配置

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

  1. // 校验maxActive、minIdle、initialSize、timeBetweenLogStatsMillis、useGlobalDataSourceStat、maxEvictableIdleTimeMillis、minEvictableIdleTimeMillis等配置是否合法
  2. // ·······
  3. // 针对oracle和DB2,需要校验validationQuery
  4. initCheck();
  5. // 当开启了testOnBorrow/testOnReturn/testWhileIdle,判断是否设置了validationQuery,没有的话会打印错误信息
  6. validationQueryCheck();

初始化ExceptionSorter、ValidConnectionChecker、JdbcDataSourceStat

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

  1. // 根据driverClassName初始化ExceptionSorter
  2. initExceptionSorter();
  3. // 根据driverClassName初始化ValidConnectionChecker
  4. initValidConnectionChecker();
  5. // 初始化dataSourceStat
  6. // 如果设置了isUseGlobalDataSourceStat为true,则支持公用监控数据
  7. if (isUseGlobalDataSourceStat()) {
  8. dataSourceStat = JdbcDataSourceStat.getGlobal();
  9. if (dataSourceStat == null) {
  10. dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbType);
  11. JdbcDataSourceStat.setGlobal(dataSourceStat);
  12. }
  13. if (dataSourceStat.getDbType() == null) {
  14. dataSourceStat.setDbType(this.dbType);
  15. }
  16. } else {
  17. dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbType, this.connectProperties);
  18. }
  19. dataSourceStat.setResetStatEnable(this.resetStatEnable);

创建initialSize数量的连接

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

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

  1. // 创建初始连接数
  2. // 异步创建,createScheduler为null,不进入
  3. if (createScheduler != null && asyncInit) {
  4. for (int i = 0; i < initialSize; ++i) {
  5. submitCreateTask(true);
  6. }
  7. // 同步创建
  8. } else if (!asyncInit) {
  9. // 创建连接的过程后面再讲
  10. while (poolingCount < initialSize) {
  11. PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
  12. DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
  13. connections[poolingCount++] = holder;
  14. }
  15. if (poolingCount > 0) {
  16. poolingPeak = poolingCount;
  17. poolingPeakTime = System.currentTimeMillis();
  18. }
  19. }

创建logStatsThread、createConnectionThread和destroyConnectionThread

这里会启动三个线程。

  1. // 启动监控数据记录线程
  2. createAndLogThread();
  3. // 启动连接创建线程
  4. createAndStartCreatorThread();
  5. // 启动连接检测线程
  6. createAndStartDestroyThread();

等待

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

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

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

  1. public class CreateConnectionThread extends Thread {
  2. public void run() {
  3. initedLatch.countDown();
  4. // do something
  5. }
  6. }

注册MBean

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

  1. // 注册MBean,用于支持JMX
  2. registerMbean();

通知createConnectionThread创建连接对象

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

  1. protected Condition empty;
  2. if (keepAlive) {
  3. // 这里会去调用empty.signal(),会去唤醒处于empty.await()状态的CreateConnectionThread
  4. this.emptySignal();
  5. }

连接对象的获取

了解下DruidPooledConnection这个类

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

概括下获取连接的过程

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

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

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

获取连接对象

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

  1. private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
  2. // 校验数据源是否可用
  3. // ······
  4. final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
  5. final int maxWaitThreadCount = this.maxWaitThreadCount;
  6. DruidConnectionHolder holder;
  7. // 加锁
  8. try {
  9. lock.lockInterruptibly();
  10. } catch(InterruptedException e) {
  11. connectErrorCountUpdater.incrementAndGet(this);
  12. throw new SQLException("interrupt", e);
  13. }
  14. try {
  15. // 判断当前等待线程是否超过maxWaitThreadCount
  16. if(maxWaitThreadCount > 0 && notEmptyWaitThreadCount >= maxWaitThreadCount) {
  17. connectErrorCountUpdater.incrementAndGet(this);
  18. throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count " + lock.getQueueLength());
  19. }
  20. // 根据是否设置maxWait选择不同的获取方式,后面选择未设置maxWait的方法来分析
  21. if(maxWait > 0) {
  22. holder = pollLast(nanos);
  23. } else {
  24. holder = takeLast();
  25. }
  26. // activeCount(所有活跃连接数量)+1,并设置峰值
  27. if(holder != null) {
  28. activeCount++;
  29. if(activeCount > activePeak) {
  30. activePeak = activeCount;
  31. activePeakTime = System.currentTimeMillis();
  32. }
  33. }
  34. } catch(InterruptedException e) {
  35. connectErrorCountUpdater.incrementAndGet(this);
  36. throw new SQLException(e.getMessage(), e);
  37. } catch(SQLException e) {
  38. connectErrorCountUpdater.incrementAndGet(this);
  39. throw e;
  40. } finally {
  41. // 解锁
  42. lock.unlock();
  43. }
  44. // 当拿到的对象为空时,抛出异常
  45. if (holder == null) {
  46. // ······
  47. }
  48. // 连接对象的useCount(使用次数)+1
  49. holder.incrementUseCount();
  50. // 包装下后返回
  51. DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
  52. return poolalbeConnection;
  53. }

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

  1. DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
  2. try {
  3. // 如果当前池中无空闲连接,因为没有设置maxWait,会一直循环地去获取
  4. while (poolingCount == 0) {
  5. // 向CreateConnectionThread发送signal,通知创建连接对象
  6. emptySignal(); // send signal to CreateThread create connection
  7. // 快速失败
  8. if (failFast && isFailContinuous()) {
  9. throw new DataSourceNotAvailableException(createError);
  10. }
  11. // notEmptyWaitThreadCount(等待连接对象的线程数)+1,并设置峰值
  12. notEmptyWaitThreadCount++;
  13. if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
  14. notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
  15. }
  16. try {
  17. // 等待连接对象创建完成或池中回收了连接对象
  18. notEmpty.await(); // signal by recycle or creator
  19. } finally {
  20. // notEmptyWaitThreadCount(等待连接对象的线程数)-1
  21. notEmptyWaitThreadCount--;
  22. }
  23. // notEmptyWaitCount(等待次数)+1
  24. notEmptyWaitCount++;
  25. }
  26. } catch (InterruptedException ie) {
  27. // TODO 这里是在notEmpty.await()时抛出的,不知为什么要notEmpty.signal()?
  28. notEmpty.signal(); // propagate to non-interrupted thread
  29. // notEmptySignalCount+1
  30. notEmptySignalCount++;
  31. throw ie;
  32. }
  33. // poolingCount(空闲连接)-1
  34. decrementPoolingCount();
  35. // 获取数组中最后一个连接对象
  36. DruidConnectionHolder last = connections[poolingCount];
  37. connections[poolingCount] = null;
  38. return last;
  39. }

创建连接对象

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

  1. public void run() {
  2. // 用于唤醒初始化数据源的线程
  3. initedLatch.countDown();
  4. long lastDiscardCount = 0;
  5. // 注意,这里是死循环,当需要创建连接对象时,这个线程会受到signal,否则会一直await
  6. for (;;) {
  7. // 加锁
  8. try {
  9. lock.lockInterruptibly();
  10. } catch (InterruptedException e2) {
  11. break;
  12. }
  13. // 丢弃数量discardCount
  14. long discardCount = DruidDataSource.this.discardCount;
  15. boolean discardChanged = discardCount - lastDiscardCount > 0;
  16. lastDiscardCount = discardCount;
  17. try {
  18. // 这个变量代表了是否有必要新增连接,true代表没必要
  19. boolean emptyWait = true;
  20. if (createError != null
  21. && poolingCount == 0
  22. && !discardChanged) {
  23. emptyWait = false;
  24. }
  25. if (emptyWait
  26. && asyncInit && createCount < initialSize) {
  27. emptyWait = false;
  28. }
  29. if (emptyWait) {
  30. // 必须存在线程等待,才创建连接
  31. if (poolingCount >= notEmptyWaitThreadCount //
  32. && (!(keepAlive && activeCount + poolingCount < minIdle))
  33. && !isFailContinuous()
  34. ) {
  35. // 等待signal,前面已经讲到,当某线程需要创建连接时,会发送signal给它
  36. empty.await();
  37. }
  38. // 防止创建超过maxActive数量的连接
  39. if (activeCount + poolingCount >= maxActive) {
  40. empty.await();
  41. continue;
  42. }
  43. }
  44. } catch (InterruptedException e) {
  45. lastCreateError = e;
  46. lastErrorTimeMillis = System.currentTimeMillis();
  47. break;
  48. } finally {
  49. // 解锁
  50. lock.unlock();
  51. }
  52. PhysicalConnectionInfo connection = null;
  53. try {
  54. // 创建原生的连接对象,并包装
  55. connection = createPhysicalConnection();
  56. } catch (SQLException e) {
  57. //出现SQLException会继续往下走
  58. //······
  59. } catch (RuntimeException e) {
  60. // 出现RuntimeException则重新进入循环体
  61. LOG.error("create connection RuntimeException", e);
  62. setFailContinuous(true);
  63. continue;
  64. } catch (Error e) {
  65. LOG.error("create connection Error", e);
  66. setFailContinuous(true);
  67. break;
  68. }
  69. // 如果为空,重新进入循环体
  70. if (connection == null) {
  71. continue;
  72. }
  73. // 将连接对象包装为DruidConnectionHolder,并放入connections数组中
  74. // 注意,该方法会去调用notEmpty.signal(),即会去唤醒正在等待获取连接的线程
  75. boolean result = put(connection);
  76. }
  77. }

testOnBorrow或testWhileIdle

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

  1. // 如果开启了testOnBorrow
  2. if (testOnBorrow) {
  3. // 这里会去调用validConnectionChecker的isValidConnection方法来校验,validConnectionChecker不存在的话,则以普通JDBC方式校验
  4. boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
  5. if (!validate) {
  6. if (LOG.isDebugEnabled()) {
  7. LOG.debug("skip not validate connection.");
  8. }
  9. Connection realConnection = poolableConnection.conn;
  10. // 丢弃连接,丢弃完会发送signal给CreateConnectionThread来创建连接
  11. discardConnection(realConnection);
  12. continue;
  13. }
  14. } else {
  15. Connection realConnection = poolableConnection.conn;
  16. if (poolableConnection.conn.isClosed()) {
  17. discardConnection(null); // 传入null,避免重复关闭
  18. continue;
  19. }
  20. if (testWhileIdle) {
  21. final DruidConnectionHolder holder = poolableConnection.holder;
  22. // 当前时间
  23. long currentTimeMillis = System.currentTimeMillis();
  24. // 最后活跃时间
  25. long lastActiveTimeMillis = holder.lastActiveTimeMillis;
  26. long lastKeepTimeMillis = holder.lastKeepTimeMillis;
  27. if (lastKeepTimeMillis > lastActiveTimeMillis) {
  28. lastActiveTimeMillis = lastKeepTimeMillis;
  29. }
  30. // 计算连接对象空闲时长
  31. long idleMillis = currentTimeMillis - lastActiveTimeMillis;
  32. long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
  33. // 空闲检测周期
  34. if (timeBetweenEvictionRunsMillis <= 0) {
  35. timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
  36. }
  37. // 当前连接空闲时长大于空间检测周期时,进入检测
  38. if (idleMillis >= timeBetweenEvictionRunsMillis
  39. || idleMillis < 0 // unexcepted branch
  40. ) {
  41. // 接下来的逻辑和前面testOnBorrow一样的
  42. boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
  43. if (!validate) {
  44. if (LOG.isDebugEnabled()) {
  45. LOG.debug("skip not validate connection.");
  46. }
  47. discardConnection(realConnection);
  48. continue;
  49. }
  50. }
  51. }
  52. }

removeAbandoned

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

  1. if (removeAbandoned) {
  2. StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
  3. poolableConnection.connectStackTrace = stackTrace;
  4. // 记录连接借出时间
  5. poolableConnection.setConnectedTimeNano();
  6. poolableConnection.traceEnable = true;
  7. activeConnectionLock.lock();
  8. try {
  9. // 放入activeConnections
  10. activeConnections.put(poolableConnection, PRESENT);
  11. } finally {
  12. activeConnectionLock.unlock();
  13. }
  14. }

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

  1. public void run() {
  2. // 检测空闲连接的phyTimeoutMillis、idleMillis是否超过指定要求
  3. shrink(true, keepAlive);
  4. // 这里会去调用DruidDataSource.removeAbandoned()进行检测
  5. if (isRemoveAbandoned()) {
  6. removeAbandoned();
  7. }
  8. }

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

  1. public int removeAbandoned() {
  2. int removeCount = 0;
  3. long currrentNanos = System.nanoTime();
  4. List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();
  5. // 加锁
  6. activeConnectionLock.lock();
  7. try {
  8. Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
  9. // 遍历借出的连接
  10. for (; iter.hasNext();) {
  11. DruidPooledConnection pooledConnection = iter.next();
  12. if (pooledConnection.isRunning()) {
  13. continue;
  14. }
  15. // 计算连接对象使用时间
  16. long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
  17. // 如果超过设置的丢弃超时时间,则加入abandonedList
  18. if (timeMillis >= removeAbandonedTimeoutMillis) {
  19. iter.remove();
  20. pooledConnection.setTraceEnable(false);
  21. abandonedList.add(pooledConnection);
  22. }
  23. }
  24. } finally {
  25. // 解锁
  26. activeConnectionLock.unlock();
  27. }
  28. // 遍历需要丢弃的连接对象
  29. if (abandonedList.size() > 0) {
  30. for (DruidPooledConnection pooledConnection : abandonedList) {
  31. final ReentrantLock lock = pooledConnection.lock;
  32. // 加锁
  33. lock.lock();
  34. try {
  35. // 如果该连接已经失效,则继续循环
  36. if (pooledConnection.isDisable()) {
  37. continue;
  38. }
  39. } finally {
  40. // 解锁
  41. lock.unlock();
  42. }
  43. // 关闭连接
  44. JdbcUtils.close(pooledConnection);
  45. pooledConnection.abandond();
  46. removeAbandonedCount++;
  47. removeCount++;
  48. }
  49. }
  50. return removeCount;
  51. }

执行filterChain

进入DruidDataSource.getConnection

  1. public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
  2. // 初始化数据源(如果还没初始化)
  3. init();
  4. // 如果设置了过滤器,会先执行每个过滤器的方法
  5. if (filters.size() > 0) {
  6. FilterChainImpl filterChain = new FilterChainImpl(this);
  7. // 这里会去递归调用过滤器的方法
  8. return filterChain.dataSource_connect(this, maxWaitMillis);
  9. } else {
  10. // 如果没有设置过滤器,直接去获取连接对象
  11. return getConnectionDirect(maxWaitMillis);
  12. }
  13. }

进入到FilterChainImpl.dataSource_connect

  1. public DruidPooledConnection dataSource_connect(DruidDataSource dataSource, long maxWaitMillis) throws SQLException {
  2. // 当指针小于过滤器数量
  3. // pos表示过滤器的索引
  4. if (this.pos < filterSize) {
  5. // 拿到第一个过滤器并调用它的dataSource_getConnection方法
  6. DruidPooledConnection conn = getFilters().get(pos++).dataSource_getConnection(this, dataSource, maxWaitMillis);
  7. return conn;
  8. }
  9. // 当访问到最后一个过滤器时,才会去创建连接
  10. return dataSource.getConnectionDirect(maxWaitMillis);
  11. }

这里以StatFilter.dataSource_getConnection为例。

  1. public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource,
  2. long maxWaitMillis) throws SQLException {
  3. // 这里又回到FilterChainImpl.dataSource_connect方法
  4. DruidPooledConnection conn = chain.dataSource_connect(dataSource, maxWaitMillis);
  5. if (conn != null) {
  6. conn.setConnectedTimeNano();
  7. StatFilterContext.getInstance().pool_connection_open();
  8. }
  9. return conn;
  10. }

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

参考资料

druid的github仓库资料

相关源码请移步:https://github.com/ZhangZiSheng001/druid-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12175893.html

源码详解系列(六) ------ 全面讲解druid的使用和源码的更多相关文章

  1. 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码

    简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...

  2. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

  3. Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节

    简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...

  4. Java源码详解系列(十)--全面分析mybatis的使用、源码和代码生成器(总计5篇博客)

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

  5. Java源码详解系列(十二)--Eureka的使用和源码

    eureka 是由 Netflix 团队开发的针对中间层服务的负载均衡器,在微服务项目中被广泛使用.相比 SLB.ALB 等负载均衡器,eureka 的服务注册是无状态的,扩展起来非常方便. 在这个系 ...

  6. 源码详解系列(五) ------ C3P0的使用和分析(包括JNDI)

    简介 c3p0是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能.目前,hibernate ...

  7. 套用GGTalk做项目的经验总结——GGTalk源码详解系列(一)

    坦白讲,我们公司其实没啥技术实力,之所以还能不断接到各种项目,全凭我们老板神通广大!要知道他每次的饭局上可都是些什么人物! 但是项目接下一大把,就凭咱哥儿几个的水平,想要独立自主.保质保量保期地一个个 ...

  8. Mybatis源码详解系列(三)--从Mapper接口开始看Mybatis的执行逻辑

    简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...

  9. Java源码详解系列(十一)--Spring的使用和源码

    Spring 是一个一站式的 Java 框架,致力于提高我们项目开发的效率.通过 Spring,我们可以避免编写大量额外代码,更专注于我们的核心逻辑.目前,Spring 已经成为最受欢迎的 Java ...

随机推荐

  1. python基础之包的导入

    包的导入 python是一门灵活性的语言 ,也可以说python是一门胶水语言,顾名思义,就是可一导入各类的包, python的包可是说是所有语言中最多的.当然导入包大部分是为了更方便,更简便,效率更 ...

  2. os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" os.environ["CUDA_VISIBLE_DEVICES"] = "0"

    os.environ[“CUDA_DEVICE_ORDER”] = “PCI_BUS_ID” # 按照PCI_BUS_ID顺序从0开始排列GPU设备 os.environ[“CUDA_VISIBLE_ ...

  3. 通过git从码云克隆项目到本地

    1.下载安装Git,傻瓜式下一步下一步即可... 2.配置Git: 2.1.选择你要clone到本地的路径:右键--->$ Git Bash Here,弹出Linux命令窗口:$ cd ~直接回 ...

  4. BraveOS正式版发布,希望大家下载使用

    废话不多说,直接贴图才是王道 这里是DOS系统+默认官方(Platform系统) 下载地址:http://pan.baidu.com/s/1eQINwx8 (引导进Platform系统后,默认管理员帐 ...

  5. Activity学习(一):生命周期

    一. 认识Activity Activity是Android的四大组件之一,那么它是什么呢?如果简单的理解,可以把它当成应用的一个显示的屏幕. Activity类处于android.app包中,继承体 ...

  6. Python--day63--单表的增删改查/GET和POST/request相关知识点回顾

  7. Codeforces Round #186 (Div. 2)

    A. Ilya and Bank Account 模拟. B. Ilya and Queries 前缀和. C. Ilya and Matrix 考虑每个元素的贡献. 边长为\(2^n\)时,贡献为最 ...

  8. H5 多媒体标签

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. 分布式全局唯一ID

    方案一.UUID UUID的方式能生成一串唯一随机32位长度数据,它是无序的一串数据,按照开放软件基金会(OSF)制定的标准计算,UUID的生成用到了以太网卡地址.纳秒级时间.芯片ID码和许多可能的数 ...

  10. JOISC2014 挂饰("01"背包)

    传送门: [1]:洛谷 [2]:BZOJ 参考资料: [1]:追忆:往昔 •题解 上述参考资料的讲解清晰易懂,下面谈谈我的理解: 关键语句: 将此题转化为 "01背包" 类问题,关 ...