druid
是用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能,另外,druid
还扩展了监控统计、防御SQL注入等功能。
本文将包含以下内容(因为篇幅较长,可根据需要选择阅读):
druid
的使用方法(入门案例、JDNI
使用、监控统计、防御SQL注入)druid
的配置参数详解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
编写druid.properties
,设置数据库连接参数和连接池基本参数等
通过DruidDataSourceFactory
加载druid.properties
文件,并创建DruidDataSource
对象
通过DruidDataSource
对象获得Connection
对象
使用Connection
对象对用户表进行增删改查
项目类型Maven Project,打包方式war(其实jar也可以,之所以使用war是为了测试JNDI
)。
这里引入日志包,主要为了看看连接池的创建过程,不引入不会有影响的。
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.20version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.15version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>commons-logginggroupId>
<artifactId>commons-loggingartifactId>
<version>1.2version>
dependency>
配置文件路径在resources
目录下,因为是入门例子,这里仅给出数据库连接参数和连接池基本参数,后面会对所有配置参数进行详细说明。另外,数据库sql
脚本也在该目录下。
当然,我们也可以通过启动参数来进行配置(但这种方式可配置参数会少一些)。
#-------------基本属性--------------------------------
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root
#数据源名,当配置多数据源时可以用于区分。注意,1.0.5版本及更早版本不支持配置该项
#默认"DataSource-" + System.identityHashCode(this)
name=zzs001
#如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
driverClassName=com.mysql.cj.jdbc.Driver
#-------------连接池大小相关参数--------------------------------
#初始化时建立物理连接的个数
#默认为0
initialSize=0
#最大连接池数量
#默认为8
maxActive=8
#最小空闲连接数量
#默认为0
minIdle=0
#已过期
#maxIdle
#获取连接时最大等待时间,单位毫秒。
#配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
#默认-1,表示无限等待
maxWait=-1
项目中编写了JDBCUtil
来初始化连接池、获取连接、管理事务和释放资源等,具体参见项目源码。
路径:cn.zzs.druid
Properties properties = new Properties();
InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(in);
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
这里以保存用户为例,路径在test目录下的cn.zzs.druid
。
@Test
public void save() throws SQLException {
// 创建sql
String sql = "insert into demo_user values(null,?,?,?,?,?)";
Connection connection = null;
PreparedStatement statement = null;
try {
// 获得连接
connection = JDBCUtils.getConnection();
// 开启事务设置非自动提交
connection.setAutoCommit(false);
// 获得Statement对象
statement = connection.prepareStatement(sql);
// 设置参数
statement.setString(1, "zzf003");
statement.setInt(2, 18);
statement.setDate(3, new Date(System.currentTimeMillis()));
statement.setDate(4, new Date(System.currentTimeMillis()));
statement.setBoolean(5, false);
// 执行
statement.executeUpdate();
// 提交事务
connection.commit();
} finally {
// 释放资源
JDBCUtils.release(connection, statement, null);
}
}
JNDI
获取数据源本文测试使用JNDI
获取DruidDataSource
对象,选择使用tomcat 9.0.21
作容器。
如果之前没有接触过JNDI
,并不会影响下面例子的理解,其实可以理解为像spring
的bean
配置和获取。
本文在入门例子的基础上增加以下依赖,因为是web
项目,所以打包方式为war
:
<dependency>
<groupId>javax.servletgroupId>
<artifactId>jstlartifactId>
<version>1.2version>
<scope>providedscope>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>javax.servlet.jspgroupId>
<artifactId>javax.servlet.jsp-apiartifactId>
<version>2.2.1version>
<scope>providedscope>
dependency>
在webapp
文件下创建目录META-INF
,并创建context.xml
文件。这里面的每个resource
节点都是我们配置的对象,类似于spring
的bean
节点。其中jdbc/druid-test
可以看成是这个bean
的id
。
注意,这里获取的数据源对象是单例的,如果希望多例,可以设置singleton="false"
。
<Context>
<Resource
name="jdbc/druid-test"
factory="com.alibaba.druid.pool.DruidDataSourceFactory"
auth="Container"
type="javax.sql.DataSource"
maxActive="15"
initialSize="3"
minIdle="3"
maxWait="10000"
url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true"
username="root"
password="root"
filters="mergeStat,log4j"
validationQuery="select 1 from dual"
/>
Context>
在web-app
节点下配置资源引用,每个resource-ref
指向了我们配置好的对象。
<resource-ref>
<res-ref-name>jdbc/druid-testres-ref-name>
<res-type>javax.sql.DataSourceres-type>
<res-auth>Containerres-auth>
resource-ref>
因为需要在web
环境中使用,如果直接建类写个main
方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp
来测试吧。
druid
提供了DruidDataSourceFactory
来支持JNDI
。
<%
String jndiName = "java:comp/env/jdbc/druid-test";
InitialContext ic = new InitialContext();
// 获取JNDI上的ComboPooledDataSource
DataSource ds = (DataSource) ic.lookup(jndiName);
JDBCUtils.setDataSource(ds);
// 创建sql
String sql = "select * from demo_user where deleted = false";
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
// 查询用户
try {
// 获得连接
connection = JDBCUtils.getConnection();
// 获得Statement对象
statement = connection.prepareStatement(sql);
// 执行
resultSet = statement.executeQuery();
// 遍历结果集
while(resultSet.next()) {
String name = resultSet.getString(2);
int age = resultSet.getInt(3);
System.err.println("用户名:" + name + ",年龄:" + age);
}
} catch(SQLException e) {
System.err.println("查询用户异常");
} finally {
// 释放资源
JDBCUtils.release(connection, statement, resultSet);
}
%>
打包项目在tomcat9
上运行,访问 http://localhost:8080/druid-demo/testJNDI.jsp ,控制台打印如下内容:
用户名:zzs001,年龄:18
用户名:zzs002,年龄:18
用户名:zzs003,年龄:25
用户名:zzf001,年龄:26
用户名:zzf002,年龄:17
用户名:zzf003,年龄:18
在以上例子基础上修改。
druid的监控统计功能是通过filter-chain
扩展实现,如果你要打开监控统计功能,配置StatFilter
,如下:
filters=stat
stat是com.alibaba.druid.filter.stat.StatFilter
的别名,别名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties
。
当你程序中存在没有参数化的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
的实现:
public class MergeStatFilter extends StatFilter {
public MergeStatFilter() {
super.setMergeSql(true);
}
}
从实现代码来看,仅仅是一个mergeSql
的缺省值。
StatFilter
属性slowSqlMillis
用来配置SQL慢的标准,执行时间超过slowSqlMillis
的就是慢。slowSqlMillis
的缺省值为3000,也就是3秒。
connectionProperties=druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000
在上面的配置中,slowSqlMillis被修改为5秒,并且通过日志输出执行慢的SQL。
缺省多个DruidDataSource
的监控数据是各自独立的,在druid-0.2.17版本之后,支持配置公用监控数据,配置参数为useGlobalDataSourceStat
。例如:
connectionProperties=druid.useGlobalDataSourceStat=true
druid内置提供了一个StatViewServlet
用于展示Druid的统计信息。
这个StatViewServlet
的用途包括:
注意:使用StatViewServlet
,建议使用druid 0.2.6以上版本。
StatViewServlet
是一个标准的javax.servlet.http.HttpServlet
,需要配置在你web应用中的WEB-INF/web.xml
中。
<servlet>
<servlet-name>DruidStatViewservlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>DruidStatViewservlet-name>
<url-pattern>/druid/*url-pattern>
servlet-mapping>
根据配置中的url-pattern来访问内置监控页面,如果是上面的配置,内置监控页面的首页是/druid/index.html
例如:
http://localhost:8080/druid-demo/druid/index.html
需要配置Servlet
的 loginUsername
和loginPassword
这两个初始参数。
示例如下:
<servlet>
<servlet-name>DruidStatViewservlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServletservlet-class>
<init-param>
<param-name>resetEnableparam-name>
<param-value>trueparam-value>
init-param>
<init-param>
<param-name>loginUsernameparam-name>
<param-value>druidparam-value>
init-param>
<init-param>
<param-name>loginPasswordparam-name>
<param-value>druidparam-value>
init-param>
servlet>
<servlet-mapping>
<servlet-name>DruidStatViewservlet-name>
<url-pattern>/druid/*url-pattern>
servlet-mapping>
StatViewSerlvet
展示出来的监控信息比较敏感,是系统运行的内部情况,如果你需要做访问控制,可以配置allow
和deny
这两个参数。比如:
<servlet>
<servlet-name>DruidStatViewservlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServletservlet-class>
<init-param>
<param-name>allowparam-name>
<param-value>128.242.127.1/24,128.242.128.1param-value>
init-param>
<init-param>
<param-name>denyparam-name>
<param-value>128.242.127.4param-value>
init-param>
servlet>
判断规则:
deny
优先于allow
,如果在deny
列表中,就算在allow
列表中,也会被拒绝。allow
没有配置或者为空,则允许所有访问在StatViewSerlvet
输出的html页面中,有一个功能是Reset All
,执行这个操作之后,会导致所有计数器清零,重新计数。你可以通过配置参数关闭它。
<servlet>
<servlet-name>DruidStatViewservlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServletservlet-class>
<init-param>
<param-name>resetEnableparam-name>
<param-value>falseparam-value>
init-param>
servlet>
WebStatFilter
用于采集web-jdbc
关联监控的数据。经常需要排除一些不必要的url,比如.js
,/jslib/
等等。配置在init-param
中。比如:
<filter>
<filter-name>DruidWebStatFilterfilter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilterfilter-class>
<init-param>
<param-name>exclusionsparam-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*param-value>
init-param>
filter>
<filter-mapping>
<filter-name>DruidWebStatFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
启动程度,访问http://localhost:8080/druid-demo/druid/index.html
,登录后可见以下页面,通过该页面我们可以查看数据源配置参数、进行SQL统计和监控,等等:
WallFilter
用于对SQL进行拦截,通过以下配置开启:
#过滤器
filters=wall,stat
注意,这种配置拦截检测的时间不在StatFilter
统计的SQL执行时间内。 如果希望StatFilter
统计的SQL执行时间内,则使用如下配置
#过滤器
filters=stat,wall
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注入类型的风险 |
druid内置提供了四种LogFilter
(Log4jFilter
、Log4j2Filter
、CommonsLogFilter
、Slf4jLogFilter
),用于输出JDBC执行的日志。这些Filter
都是Filter-Chain
扩展机制中的Filter
,所以配置方式可以参考这里:
#过滤器
filters=log4j
在druid-xxx.jar!/META-INF/druid-filter.properties
文件中描述了这四种Filter的别名:
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter
druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter
druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter
druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
他们的别名分别是log4j
、log4j2
、slf4j
、commonlogging
和commonLogging
。其中commonlogging
和commonLogging
只是大小写不同。
缺省输入的日志信息全面,但是内容比较多,有时候我们需要定制化配置日志输出。
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
,可以通过log4j.properties
文件配置日志输出选项,例如:
log4j.logger.druid.sql=warn,stdout
log4j.logger.druid.sql.DataSource=warn,stdout
log4j.logger.druid.sql.Connection=warn,stdout
log4j.logger.druid.sql.Statement=warn,stdout
log4j.logger.druid.sql.ResultSet=warn,stdout
参数配置方式
connectionProperties=druid.log.stmt.executableSql=true
使用druid,同一个参数,我们可以采用多种方式进行配置,举个例子:maxActive
(最大连接池参数)的配置:
系统属性一般在启动参数中设置。通过方式一来配置连接池参数的还是比较少见。
-Ddruid.maxActive=8
这是最常见的一种。
maxActive=8
相比第二种方式,这里只是加了.druid
前缀。
druid.maxActive=8
connectionProperties
可以用于配置多个属性,不同属性使用";"隔开。
connectionProperties=druid.maxActive=8
connectProperties
可以在方式一、方式三和方式四中存在,具体配置如下:
# 方式一
-Ddruid.connectProperties=druid.maxActive=8
# 方式三:支持多个属性,不同属性使用";"隔开
druid.connectProperties=druid.maxActive=8
# 方式四
connectionProperties=druid.connectProperties=druid.maxActive=8
这个属性甚至可以这样配(当然应该没人会这么做):
druid.connectProperties=druid.connectProperties=druid.connectProperties=druid.connectProperties=druid.maxActive=8
真的是没完没了,怎么会引入connectProperties
这个属性呢?我觉得这是一个十分失败的设计,所以本文仅会讲前面说的四种。
前面已经讲到,同一个参数,我们有时可以采用无数种方式来配置。表面上看这样设计十分人性化,可以适应不同人群的使用习惯,但是,在我看来,这样设计非常不利于配置的统一管理,另外,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'
。
#-------------基本属性--------------------------------
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root
#数据源名,当配置多数据源时可以用于区分。注意,1.0.5版本及更早版本不支持配置该项
#默认"DataSource-" + System.identityHashCode(this)
name=zzs001
#如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
driverClassName=com.mysql.cj.jdbc.Driver
这几个参数都比较常用,具体设置多少需根据项目调整。
#-------------连接池大小相关参数--------------------------------
#初始化时建立物理连接的个数
#默认为0
initialSize=0
#最大连接池数量
#默认为8
maxActive=8
#最小空闲连接数量
#默认为0
minIdle=0
#已过期
#maxIdle
#获取连接时最大等待时间,单位毫秒。
#配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
#默认-1,表示无限等待
maxWait=-1
针对连接失效的问题,建议开启空闲连接测试,而不建议开启借出测试(从性能考虑),另外,开启连接测试时,必须配置validationQuery
。
#-------------连接检测情况--------------------------------
#用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。
#如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
#默认为空
validationQuery=select 1 from dual
#检测连接是否有效的超时时间,单位:秒。
#底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
#默认-1
validationQueryTimeout=-1
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
#默认为false
testOnBorrow=false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
#默认为false
testOnReturn=false
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
#建议配置为true,不影响性能,并且保证安全性。
#默认为true
testWhileIdle=true
#有两个含义:
#1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
#2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
#默认1000*60
timeBetweenEvictionRunsMillis=-1
#不再使用,一个DruidDataSource只支持一个EvictionRun
#numTestsPerEvictionRun=3
#连接保持空闲而不被驱逐的最小时间。
#默认值1000*60*30 = 30分钟
minEvictableIdleTimeMillis=1800000
针对大部分数据库而言,开启缓存语句可以有效提高性能,但是在myslq下建议关闭。
#-------------缓存语句--------------------------------
#是否缓存preparedStatement,也就是PSCache。
#PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭
#默认为false
poolPreparedStatements=false
#PSCache的最大个数。
#要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
#在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
#默认为10
maxOpenPreparedStatements=10
建议保留默认就行。
#-------------事务相关的属性--------------------------------
#连接池创建的连接的默认的auto-commit状态
#默认为空,由驱动决定
defaultAutoCommit=true
#连接池创建的连接的默认的read-only状态。
#默认值为空,由驱动决定
defaultReadOnly=false
#连接池创建的连接的默认的TransactionIsolation状态
#可用值为下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
#默认值为空,由驱动决定
defaultTransactionIsolation=REPEATABLE_READ
#连接池创建的连接的默认的数据库名
defaultCatalog=github_demo
#-------------连接泄漏回收参数--------------------------------
#当未使用的时间超过removeAbandonedTimeout时,是否视该连接为泄露连接并删除
#默认为false
removeAbandoned=false
#泄露的连接可以被删除的超时值, 单位毫秒
#默认为300*1000
removeAbandonedTimeoutMillis=300*1000
#标记当Statement或连接被泄露时是否打印程序的stack traces日志。
#默认为false
logAbandoned=true
#连接最大存活时间
#默认-1
#phyTimeoutMillis=-1
#-------------过滤器--------------------------------
#属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
#别名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties
#监控统计用的filter:stat(mergeStat可以合并sql)
#日志用的filter:log4j
#防御sql注入的filter:wall
filters=log4j,wall,mergeStat
#用于设置filter、exceptionSorter、validConnectionChecker等的属性
#多个参数用";"隔开
connectionProperties=druid.useGlobalDataSourceStat=true;druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000
#-------------其他--------------------------------
#控制PoolGuard是否容许获取底层连接
#默认为false
accessToUnderlyingConnectionAllowed=false
#当数据库抛出一些不可恢复的异常时,抛弃连接
#根据dbType自动识别
#exceptionSorter
#exception-sorter-class-name=
#物理连接初始化的时候执行的sql
#initConnectionSqls=
#是否创建数据源时就初始化连接池
init=true
看过druid的源码就会发现,相比其他DBCP和C3P0,druid有以下特点:
CountDownLatch
、ReentrantLock
、AtomicLongFieldUpdater
、Condition
等,也就是说,在分析druid源码之前,最好先学习下这些技术;DruidDataSource
里面。另外,在对类或接口的抽象上,个人感觉,druid不是很“面向对象”,有的接口或类的方法很难统一成某种对象的行为,所以,本文不会去关注类的设计,更多地将分析一些重要功能的实现。注意:考虑篇幅和可读性,以下代码经过删减,仅保留所需部分。
前面已经讲过,druid为我们提供了“无数”种方式来配置参数,这里我再补充下不同配置方式的加载顺序(当然,只会涉及到四种方式)。
当我们使用调用DruidDataSourceFactory.createDataSource(Properties)
时,会加载配置来给对应的属性赋值,另外,这个过程还会根据配置去创建对应的过滤器。不同配置方式加载时机不同,后者会覆盖已存在的相同参数,如图所示。
这里先来介绍下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()
这个方法,可以概括为以下步骤:
initStackTrace
、id
、xxIdSeed
、dbTyp
、driver
、dataSourceStat
、connections
、evictConnections
、keepAliveConnections
等属性maxActive
、minIdle
、initialSize
、timeBetweenLogStatsMillis
、useGlobalDataSourceStat
、maxEvictableIdleTimeMillis
、minEvictableIdleTimeMillis
、validationQuery
等配置是否合法ExceptionSorter
、ValidConnectionChecker
、JdbcDataSourceStat
initialSize
数量的连接logStatsThread
、createConnectionThread
和destroyConnectionThread
createConnectionThread
和destroyConnectionThread
线程run后再继续执行MBean
,用于支持JMXkeepAlive
,通知createConnectionThread
创建连接对象这个方法差不多200行,考虑篇幅,我删减了部分内容。
druid数据源初始化采用的是ReentrantLock
,如下:
final ReentrantLock lock = this.lock;
try {
// 加锁
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}
boolean init = false;
try {
// do something
} finally {
inited = true;
// 解锁
lock.unlock();
}
注意,以下步骤均在这个锁的范围内。
这部分内容主要是初始化一些属性,需要注意的一点就是,这里使用了AtomicLongFieldUpdater
来进行原子更新,保证写的安全和读的高效,当然,还是cocurrent
包的工具。
// 这里使用了AtomicLongFieldUpdater来进行原子更新,保证了写的安全和读的高效
this.id = DruidDriver.createDataSourceId();
if (this.id > 1) {
long delta = (this.id - 1) * 100000;
this.connectionIdSeedUpdater.addAndGet(this, delta);
this.statementIdSeedUpdater.addAndGet(this, delta);
this.resultSetIdSeedUpdater.addAndGet(this, delta);
this.transactionIdSeedUpdater.addAndGet(this, delta);
}
// 设置url
if (this.jdbcUrl != null) {
this.jdbcUrl = this.jdbcUrl.trim();
// 针对druid自定义的一种url格式,进行解析
// jdbc:wrap-jdbc:开头,可设置driver、name、jmx等
initFromWrapDriverUrl();
}
// 根据url前缀,确定dbType
if (this.dbType == null || this.dbType.length() == 0) {
this.dbType = JdbcUtils.getDbType(jdbcUrl, null);
}
// cacheServerConfiguration,暂时不知道这个参数干嘛用的
if (JdbcConstants.MYSQL.equals(this.dbType)
|| JdbcConstants.MARIADB.equals(this.dbType)
|| JdbcConstants.ALIYUN_ADS.equals(this.dbType)) {
boolean cacheServerConfigurationSet = false;
if (this.connectProperties.containsKey("cacheServerConfiguration")) {
cacheServerConfigurationSet = true;
} else if (this.jdbcUrl.indexOf("cacheServerConfiguration") != -1) {
cacheServerConfigurationSet = true;
}
if (cacheServerConfigurationSet) {
this.connectProperties.put("cacheServerConfiguration", "true");
}
}
// 设置驱动类
if (this.driverClass != null) {
this.driverClass = driverClass.trim();
}
// 如果我们没有配置driverClass
if (this.driver == null) {
// 根据url识别对应的driverClass
if (this.driverClass == null || this.driverClass.isEmpty()) {
this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
}
// MockDriver的情况,这里不讨论
if (MockDriver.class.getName().equals(driverClass)) {
driver = MockDriver.instance;
} else {
if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) {
throw new SQLException("url not set");
}
// 创建Driver实例,注意,这个过程不需要依赖DriverManager
driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
}
} else {
if (this.driverClass == null) {
this.driverClass = driver.getClass().getName();
}
}
// 用于存放所有连接对象
connections = new DruidConnectionHolder[maxActive];
// 用于存放需要丢弃的连接对象
evictConnections = new DruidConnectionHolder[maxActive];
// 用于存放需要keepAlive的连接对象
keepAliveConnections = new DruidConnectionHolder[maxActive];
看到下面的代码会发现,我们还可以通过SPI机制来配置过滤器。
使用SPI配置过滤器时需要注意,对应的类需要加上@AutoLoad
注解,另外还需要配置load.spifilter.skip=false
,SPI相关内容可参考我的另一篇博客:使用SPI解耦你的实现类。
在这个方法里,主要就是初始化过滤器的一些属性而已。过滤器的部分,本文不会涉及到太多。
// 初始化filters
for (Filter filter : filters) {
filter.init(this);
}
// 采用SPI机制加载过滤器,这部分过滤器除了放入filters,还会放入autoFilters
initFromSPIServiceLoader();
这里只是简单的校验,不涉及太多复杂的逻辑。
// 校验maxActive、minIdle、initialSize、timeBetweenLogStatsMillis、useGlobalDataSourceStat、maxEvictableIdleTimeMillis、minEvictableIdleTimeMillis等配置是否合法
// ·······
// 针对oracle和DB2,需要校验validationQuery
initCheck();
// 当开启了testOnBorrow/testOnReturn/testWhileIdle,判断是否设置了validationQuery,没有的话会打印错误信息
validationQueryCheck();
这里重点关注ExceptionSorter
和ValidConnectionChecker
这两个类,这里会根据数据库类型进行选择。其中,ValidConnectionChecker
用于对连接进行检测。
// 根据driverClassName初始化ExceptionSorter
initExceptionSorter();
// 根据driverClassName初始化ValidConnectionChecker
initValidConnectionChecker();
// 初始化dataSourceStat
// 如果设置了isUseGlobalDataSourceStat为true,则支持公用监控数据
if (isUseGlobalDataSourceStat()) {
dataSourceStat = JdbcDataSourceStat.getGlobal();
if (dataSourceStat == null) {
dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbType);
JdbcDataSourceStat.setGlobal(dataSourceStat);
}
if (dataSourceStat.getDbType() == null) {
dataSourceStat.setDbType(this.dbType);
}
} else {
dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbType, this.connectProperties);
}
dataSourceStat.setResetStatEnable(this.resetStatEnable);
这里有两种方式创建连接,一种是异步,一种是同步。但是,根据我们的使用例子,createScheduler
为null,所以采用的是同步的方式。
注意,后面的所有代码也是基于createScheduler
为null来分析的。
// 创建初始连接数
// 异步创建,createScheduler为null,不进入
if (createScheduler != null && asyncInit) {
for (int i = 0; i < initialSize; ++i) {
submitCreateTask(true);
}
// 同步创建
} else if (!asyncInit) {
// 创建连接的过程后面再讲
while (poolingCount < initialSize) {
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
connections[poolingCount++] = holder;
}
if (poolingCount > 0) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
}
这里会启动三个线程。
// 启动监控数据记录线程
createAndLogThread();
// 启动连接创建线程
createAndStartCreatorThread();
// 启动连接检测线程
createAndStartDestroyThread();
这里使用了CountDownLatch
,保证当createConnectionThread
和destroyConnectionThread
开始run时再继续执行。
private final CountDownLatch initedLatch = new CountDownLatch(2);
// 线程进入等待,等待CreatorThread和DestroyThread执行
initedLatch.await();
我们进入到DruidDataSource.CreateConnectionThread.run()
,可以看到,一执行run方法就会调用countDown
。destroyConnectionThread
也是一样,这里就不放进来了。
public class CreateConnectionThread extends Thread {
public void run() {
initedLatch.countDown();
// do something
}
}
接下来是注册MBean
,会去注册DruidDataSourceStatManager
和DruidDataSource
,启动我们的程度,通过jconsole就可以看到这两个MBean
。JMX相关内容这里就不多扩展了,感兴趣的话可参考我的另一篇博客: 如何使用JMX来管理程序?
// 注册MBean,用于支持JMX
registerMbean();
前面已经讲过,当我们调用empty.signal()
,会去唤醒处于empty.await()
状态的CreateConnectionThread
。CreateConnectionThread
这个线只有在需要创建连接时才运行,否则会一直等待,后面会讲到。
protected Condition empty;
if (keepAlive) {
// 这里会去调用empty.signal(),会去唤醒处于empty.await()状态的CreateConnectionThread
this.emptySignal();
}
用户调用DruidDataSource.getConnection
,拿到的对象时DruidPooledConnection
,里面封装了DruidConnectionHolder
,而这个对象包含了原生的连接对象和我们一开始创建的数据源对象。
连接对象的获取过程可以概括为以下步骤:
createConnectionThread
发送signal创建新连接,此时会进入等待;testOnBorrow
,进行testOnBorrow
检测,否则,如果设置了testWhileIdle
,进行testWhileIdle
检测;removeAbandoned
,则会将连接对象放入activeConnections
;defaultAutoCommit
,并返回;filterChain
。初始化数据源的前面已经讲过了,这里就直接从第二步开始。
进入DruidDataSource.getConnectionInternal
方法。除了获取连接对象,其他的大部分是校验和计数的内容。
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
// 校验数据源是否可用
// ······
final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
final int maxWaitThreadCount = this.maxWaitThreadCount;
DruidConnectionHolder holder;
// 加锁
try {
lock.lockInterruptibly();
} catch(InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("interrupt", e);
}
try {
// 判断当前等待线程是否超过maxWaitThreadCount
if(maxWaitThreadCount > 0 && notEmptyWaitThreadCount >= maxWaitThreadCount) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count " + lock.getQueueLength());
}
// 根据是否设置maxWait选择不同的获取方式,后面选择未设置maxWait的方法来分析
if(maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
// activeCount(所有活跃连接数量)+1,并设置峰值
if(holder != null) {
activeCount++;
if(activeCount > activePeak) {
activePeak = activeCount;
activePeakTime = System.currentTimeMillis();
}
}
} catch(InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException(e.getMessage(), e);
} catch(SQLException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw e;
} finally {
// 解锁
lock.unlock();
}
// 当拿到的对象为空时,抛出异常
if (holder == null) {
// ······
}
// 连接对象的useCount(使用次数)+1
holder.incrementUseCount();
// 包装下后返回
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
return poolalbeConnection;
}
下面再看下DruidDataSource.takeLast()
方法(即没有配置maxWait时调用的方法)。该方法中,当没有空闲连接对象时,会尝试创建连接,此时该线程进入等待(notEmpty.await()
),只有连接对象创建完成或池中回收了连接对象(notEmpty.signal()
),该线程才会继续执行。
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
// 如果当前池中无空闲连接,因为没有设置maxWait,会一直循环地去获取
while (poolingCount == 0) {
// 向CreateConnectionThread发送signal,通知创建连接对象
emptySignal(); // send signal to CreateThread create connection
// 快速失败
if (failFast && isFailContinuous()) {
throw new DataSourceNotAvailableException(createError);
}
// notEmptyWaitThreadCount(等待连接对象的线程数)+1,并设置峰值
notEmptyWaitThreadCount++;
if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
}
try {
// 等待连接对象创建完成或池中回收了连接对象
notEmpty.await(); // signal by recycle or creator
} finally {
// notEmptyWaitThreadCount(等待连接对象的线程数)-1
notEmptyWaitThreadCount--;
}
// notEmptyWaitCount(等待次数)+1
notEmptyWaitCount++;
}
} catch (InterruptedException ie) {
// TODO 这里是在notEmpty.await()时抛出的,不知为什么要notEmpty.signal()?
notEmpty.signal(); // propagate to non-interrupted thread
// notEmptySignalCount+1
notEmptySignalCount++;
throw ie;
}
// poolingCount(空闲连接)-1
decrementPoolingCount();
// 获取数组中最后一个连接对象
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
return last;
}
前面已经讲到,创建连接是采用异步方式,进入到DruidDataSource.CreateConnectionThread.run()
。当不需要创建连接时,该线程进入empty.await()
状态,此时需要用户线程调用empty.signal()
来唤醒。
public void run() {
// 用于唤醒初始化数据源的线程
initedLatch.countDown();
long lastDiscardCount = 0;
// 注意,这里是死循环,当需要创建连接对象时,这个线程会受到signal,否则会一直await
for (;;) {
// 加锁
try {
lock.lockInterruptibly();
} catch (InterruptedException e2) {
break;
}
// 丢弃数量discardCount
long discardCount = DruidDataSource.this.discardCount;
boolean discardChanged = discardCount - lastDiscardCount > 0;
lastDiscardCount = discardCount;
try {
// 这个变量代表了是否有必要新增连接,true代表没必要
boolean emptyWait = true;
if (createError != null
&& poolingCount == 0
&& !discardChanged) {
emptyWait = false;
}
if (emptyWait
&& asyncInit && createCount < initialSize) {
emptyWait = false;
}
if (emptyWait) {
// 必须存在线程等待,才创建连接
if (poolingCount >= notEmptyWaitThreadCount //
&& (!(keepAlive && activeCount + poolingCount < minIdle))
&& !isFailContinuous()
) {
// 等待signal,前面已经讲到,当某线程需要创建连接时,会发送signal给它
empty.await();
}
// 防止创建超过maxActive数量的连接
if (activeCount + poolingCount >= maxActive) {
empty.await();
continue;
}
}
} catch (InterruptedException e) {
lastCreateError = e;
lastErrorTimeMillis = System.currentTimeMillis();
break;
} finally {
// 解锁
lock.unlock();
}
PhysicalConnectionInfo connection = null;
try {
// 创建原生的连接对象,并包装
connection = createPhysicalConnection();
} catch (SQLException e) {
//出现SQLException会继续往下走
//······
} catch (RuntimeException e) {
// 出现RuntimeException则重新进入循环体
LOG.error("create connection RuntimeException", e);
setFailContinuous(true);
continue;
} catch (Error e) {
LOG.error("create connection Error", e);
setFailContinuous(true);
break;
}
// 如果为空,重新进入循环体
if (connection == null) {
continue;
}
// 将连接对象包装为DruidConnectionHolder,并放入connections数组中
// 注意,该方法会去调用notEmpty.signal(),即会去唤醒正在等待获取连接的线程
boolean result = put(connection);
}
}
进入DruidDataSource.getConnectionDirect(long)
。该方法会使用到validConnectionChecker
来校验连接的有效性。
// 如果开启了testOnBorrow
if (testOnBorrow) {
// 这里会去调用validConnectionChecker的isValidConnection方法来校验,validConnectionChecker不存在的话,则以普通JDBC方式校验
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
Connection realConnection = poolableConnection.conn;
// 丢弃连接,丢弃完会发送signal给CreateConnectionThread来创建连接
discardConnection(realConnection);
continue;
}
} else {
Connection realConnection = poolableConnection.conn;
if (poolableConnection.conn.isClosed()) {
discardConnection(null); // 传入null,避免重复关闭
continue;
}
if (testWhileIdle) {
final DruidConnectionHolder holder = poolableConnection.holder;
// 当前时间
long currentTimeMillis = System.currentTimeMillis();
// 最后活跃时间
long lastActiveTimeMillis = holder.lastActiveTimeMillis;
long lastKeepTimeMillis = holder.lastKeepTimeMillis;
if (lastKeepTimeMillis > lastActiveTimeMillis) {
lastActiveTimeMillis = lastKeepTimeMillis;
}
// 计算连接对象空闲时长
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
// 空闲检测周期
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
// 当前连接空闲时长大于空间检测周期时,进入检测
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
// 接下来的逻辑和前面testOnBorrow一样的
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(realConnection);
continue;
}
}
}
}
进入DruidDataSource.getConnectionDirect(long)
,这里不会进行检测,只是将连接对象放入activeConnections
,具体泄露连接的检测工作是在DestroyConnectionThread
线程中进行。
if (removeAbandoned) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
// 记录连接借出时间
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
activeConnectionLock.lock();
try {
// 放入activeConnections
activeConnections.put(poolableConnection, PRESENT);
} finally {
activeConnectionLock.unlock();
}
}
DestroyConnectionThread
线程会根据我们设置的timeBetweenEvictionRunsMillis
来进行检验,具体的校验会去运行DestroyTask
(DruidDataSource
的内部类),这里看下DestroyTask
的run
方法。
public void run() {
// 检测空闲连接的phyTimeoutMillis、idleMillis是否超过指定要求
shrink(true, keepAlive);
// 这里会去调用DruidDataSource.removeAbandoned()进行检测
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
进入DruidDataSource.removeAbandoned()
,当连接对象使用时间超过removeAbandonedTimeoutMillis
,则会被丢弃掉。
public int removeAbandoned() {
int removeCount = 0;
long currrentNanos = System.nanoTime();
List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();
// 加锁
activeConnectionLock.lock();
try {
Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
// 遍历借出的连接
for (; iter.hasNext();) {
DruidPooledConnection pooledConnection = iter.next();
if (pooledConnection.isRunning()) {
continue;
}
// 计算连接对象使用时间
long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
// 如果超过设置的丢弃超时时间,则加入abandonedList
if (timeMillis >= removeAbandonedTimeoutMillis) {
iter.remove();
pooledConnection.setTraceEnable(false);
abandonedList.add(pooledConnection);
}
}
} finally {
// 解锁
activeConnectionLock.unlock();
}
// 遍历需要丢弃的连接对象
if (abandonedList.size() > 0) {
for (DruidPooledConnection pooledConnection : abandonedList) {
final ReentrantLock lock = pooledConnection.lock;
// 加锁
lock.lock();
try {
// 如果该连接已经失效,则继续循环
if (pooledConnection.isDisable()) {
continue;
}
} finally {
// 解锁
lock.unlock();
}
// 关闭连接
JdbcUtils.close(pooledConnection);
pooledConnection.abandond();
removeAbandonedCount++;
removeCount++;
}
}
return removeCount;
}
进入DruidDataSource.getConnection
。
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
// 初始化数据源(如果还没初始化)
init();
// 如果设置了过滤器,会先执行每个过滤器的方法
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
// 这里会去递归调用过滤器的方法
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
// 如果没有设置过滤器,直接去获取连接对象
return getConnectionDirect(maxWaitMillis);
}
}
进入到FilterChainImpl.dataSource_connect
。
public DruidPooledConnection dataSource_connect(DruidDataSource dataSource, long maxWaitMillis) throws SQLException {
// 当指针小于过滤器数量
// pos表示过滤器的索引
if (this.pos < filterSize) {
// 拿到第一个过滤器并调用它的dataSource_getConnection方法
DruidPooledConnection conn = getFilters().get(pos++).dataSource_getConnection(this, dataSource, maxWaitMillis);
return conn;
}
// 当访问到最后一个过滤器时,才会去创建连接
return dataSource.getConnectionDirect(maxWaitMillis);
}
这里以StatFilter.dataSource_getConnection
为例。
public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource,
long maxWaitMillis) throws SQLException {
// 这里又回到FilterChainImpl.dataSource_connect方法
DruidPooledConnection conn = chain.dataSource_connect(dataSource, maxWaitMillis);
if (conn != null) {
conn.setConnectedTimeNano();
StatFilterContext.getInstance().pool_connection_open();
}
return conn;
}
以上,druid的源码基本已经分析完,其他部分内容有空再做补充。
druid的github仓库资料
相关源码请移步:https://github.com/ZhangZiSheng001/druid-demo
本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12175893.html