druid
是用于建立和管理链接,利用“池”的方式复用链接减小资源开销,和其余数据源同样,也具备链接数控制、链接可靠性测试、链接泄露控制、缓存语句等功能,另外,druid
还扩展了监控统计、防护SQL注入等功能。css
本文将包含如下内容(由于篇幅较长,可根据须要选择阅读):html
druid
的使用方法(入门案例、JDNI
使用、监控统计、防护SQL注入)druid
的配置参数详解druid
主要源码分析其余链接池的内容也能够参考个人其余博客:java
源码详解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)mysql
源码详解系列(五) ------ C3P0的使用和分析(包括JNDI)git
使用druid
链接池获取链接对象,对用户数据进行简单的增删改查(sql
脚本项目中已提供)。github
JDK
:1.8.0_231web
maven
:3.6.1spring
IDE
:eclipse 4.12sql
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>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> <!-- log --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </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() { // 建立sql String sql = "insert into demo_user values(null,?,?,?,?,?)"; Connection connection = null; PreparedStatement statement = null; try { // 得到链接 connection = JDBCUtil.getConnection(); // 开启事务设置非自动提交 JDBCUtil.startTrasaction(); // 得到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(); // 提交事务 JDBCUtil.commit(); } catch(Exception e) { JDBCUtil.rollback(); log.error("保存用户失败", e); } finally { // 释放资源 JDBCUtil.release(connection, statement, null); } }
JNDI
获取数据源本文测试使用JNDI
获取DruidDataSource
对象,选择使用tomcat 9.0.21
做容器。
若是以前没有接触过JNDI
,并不会影响下面例子的理解,其实能够理解为像spring
的bean
配置和获取。
本文在入门例子的基础上增长如下依赖,由于是web
项目,因此打包方式为war
:
<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.2.1</version> <scope>provided</scope> </dependency>
在webapp
文件下建立目录META-INF
,并建立context.xml
文件。这里面的每一个resource
节点都是咱们配置的对象,相似于spring
的bean
节点。其中jdbc/druid-test
能够当作是这个bean
的id
。
注意,这里获取的数据源对象是单例的,若是但愿多例,能够设置singleton="false"
。
<?xml version="1.0" encoding="UTF-8"?> <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
指向了咱们配置好的对象。
<!-- JNDI数据源 --> <resource-ref> <res-ref-name>jdbc/druid-test</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>
由于须要在web
环境中使用,若是直接建类写个main
方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp
来测试吧。
druid
提供了DruidDataSourceFactory
来支持JNDI
。
<body> <% 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); } %> </body>
打包项目在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>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>
根据配置中的url-pattern来访问内置监控页面,若是是上面的配置,内置监控页面的首页是/druid/index.html
例如:
http://localhost:8080/druid-demo/druid/index.html
须要配置Servlet
的 loginUsername
和loginPassword
这两个初始参数。
示例以下:
<!-- 配置 Druid 监控信息显示页面 --> <servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <!-- 容许清空统计数据 --> <param-name>resetEnable</param-name> <param-value>true</param-value> </init-param> <init-param> <!-- 用户名 --> <param-name>loginUsername</param-name> <param-value>druid</param-value> </init-param> <init-param> <!-- 密码 --> <param-name>loginPassword</param-name> <param-value>druid</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>DruidStatView</servlet-name> <url-pattern>/druid/*</url-pattern> </servlet-mapping>
StatViewSerlvet
展现出来的监控信息比较敏感,是系统运行的内部状况,若是你须要作访问控制,能够配置allow
和deny
这两个参数。好比:
<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <param-name>allow</param-name> <param-value>128.242.127.1/24,128.242.128.1</param-value> </init-param> <init-param> <param-name>deny</param-name> <param-value>128.242.127.4</param-value> </init-param> </servlet>
判断规则:
deny
优先于allow
,若是在deny
列表中,就算在allow
列表中,也会被拒绝。allow
没有配置或者为空,则容许全部访问在StatViewSerlvet
输出的html页面中,有一个功能是Reset All
,执行这个操做以后,会致使全部计数器清零,从新计数。你能够经过配置参数关闭它。
<servlet> <servlet-name>DruidStatView</servlet-name> <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class> <init-param> <param-name>resetEnable</param-name> <param-value>false</param-value> </init-param> </servlet>
WebStatFilter
用于采集web-jdbc
关联监控的数据。常常须要排除一些没必要要的url,好比.js
,/jslib/
等等。配置在init-param
中。好比:
<filter> <filter-name>DruidWebStatFilter</filter-name> <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class> <init-param> <param-name>exclusions</param-name> <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value> </init-param> </filter> <filter-mapping> <filter-name>DruidWebStatFilter</filter-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
能够在方式1、方式三和方式四中存在,具体配置以下:
# 方式一 -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的源码基本已经分析完,其余部份内容有空再作补充。
相关源码请移步:https://github.com/ZhangZiSheng001/druid-demo
本文为原创文章,转载请附上原文出处连接:https://www.cnblogs.com/ZhangZiSheng001/p/12175893.html