Druid介绍
Druid首先是一个数据库连接池,并且是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQL Parser。
Druid支持所有JDBC兼容的数据库,包括Oracle、MySQL、Derby、Postgresql、SQL Server、H2等等,并且Druid针对Oracle和MySql做了特别优化,比如Oracle的PS Cache内存占用优化,MySql的ping检测优化。
通过Druid提供的监控功能,监控SQL的执行时间、ResultSet持有时间、返回行数、更新行数、错误次数、错误堆栈信息,可以清楚知道连接池和SQL的工作情况,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
Druid配置
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.20</version>
</dependency>
数据源的配置:
<!-- 引入配置文件 -->
<bean id="mybatisPropertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1" />
<property name="ignoreUnresolvablePlaceholders" value="true" />
<property name="locations">
<list>
<value>classpath:druid.properties</value>
</list>
</property>
</bean>
<!-- druid连接池配置 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<property name="filters" value="${filters}" />
<property name="maxActive" value="${maxActive}" />
<property name="initialSize" value="${initialSize}" />
<property name="maxWait" value="${maxWait}" />
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${validationQuery}" />
<property name="testWhileIdle" value="${testWhileIdle}" />
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="testOnReturn" value="${testOnReturn}" />
<property name="poolPreparedStatements" value="${poolPreparedStatements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${maxPoolPreparedStatementPerConnectionSize}" />
</bean>
druid.properties的内容:
url=jdbc:mysql://localhost:3306/era
username=root
password=123456
#初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initialSize =1
#定义最大连接池数量
maxActive=20
#获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
maxWait=60000
#是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。
#在mysql5.5以下的版本中没有PSCache功能,建议关闭掉。5.5及以上版本有PSCache,建议开启。
poolPreparedStatements=false
#要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
#在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
maxPoolPreparedStatementPerConnectionSize=100
#用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
validationQuery=SELECT 'x'
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow=false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn=false
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle=true
#属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall
filters=stat,wall
#有两个含义:1) Destroy线程会检测连接的间隔时间;2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
timeBetweenEvictionRunsMillis=3000
#配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis=300000
如果要使用Druid的内置监控功能,需要配置数据源时加上<property name="filters" value="stat" />,上面已经有了。
还需要在web.xml中加上:
<!-- 启用Web监控统计功能需要在Web应用的web.xml中加入这个Servlet声明 -->
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
<init-param>
<!-- 是否允许清空统计数据,不写时默认true -->
<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>123456</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
启动项目后在浏览器输入:
http://ip:port/项目名/druid/或http://ip:port/项目名/druid/index.html即可访问。
问题描述
程序部署在生产环境(数据库是oracle),大概半个小时不用,再次使用的时候就出现查询数据库卡主,停顿大概15分钟左右,15分钟之后程序抛出异常,然后再使用就可以了,多刷新几次,跳过当前有问题的那个数据库连接也能正常使用,最后解决的方法是添加了一个参数,参数名字是:keepAlive
github druid的上对这个参数的说明
https://github.com/alibaba/druid/wiki/KeepAlive_cn
问题解决办法:
第一步:
druidDataSource.setConnectionProperties("oracle.net.CONNECT_TIMEOUT=2000;oracle.jdbc.ReadTimeout=30000");
//后面的ReadTimeout单位也是毫秒,不要设置的太短,否则有些sql执行时间本来就长,报错会影响正常使用。mysql可以这样写:
druidDataSource.setConnectionProperties("connectTimeout=2000;socketTimeout=30000);
sqlserver或postgresql可以这写:
druidDataSource.setConnectionProperties("loginTimeout=2000;socketTimeout=30000);
第二步:
druidDataSource.setMinEvictableIdleTimeMillis(180000);
//配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000
druidDataSource.setKeepAlive(true);
//打开druid.keepAlive之后,当连接池空闲时,池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作,即执行druid.validationQuery指定的查询SQL,一般为select 1 from dual,其他数据库一般为select 1,只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间,就可以保证当连接空闲时自动做保活检测,不会被防火墙切断
数据源的初始化
了解下DruidDataSource这个类
这里先来介绍下DruidDataSource这个类:
DruidDataSource的UML图
图中我只列出了几个重要的属性,这几个属性没有理解好,后面的源码很难看得进去。
概括下初始化的过程
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,用于支持JMX
- 如果设置了keepAlive,通知createConnectionThread创建连接对象
- 解锁
这个方法差不多200行,考虑篇幅,我删减了部分内容。
加锁和解锁
druid数据源初始化采用的是ReentrantLock,如下:
注意,以下步骤均在这个锁的范围内。
初始化属性
这部分内容主要是初始化一些属性,需要注意的一点就是,这里使用了AtomicLongFieldUpdater来进行原子更新,保证写的安全和读的高效,当然,还是cocurrent包的工具。
初始化过滤器
看到下面的代码会发现,我们还可以通过SPI机制来配置过滤器。
使用SPI配置过滤器时需要注意,对应的类需要加上@AutoLoad注解,另外还需要配置load.spifilter.skip=false,SPI相关内容可参考我的另一篇博客:使用SPI解耦你的实现类。
在这个方法里,主要就是初始化过滤器的一些属性而已。过滤器的部分,本文不会涉及到太多。
校验配置
这里只是简单的校验,不涉及太多复杂的逻辑。
初始化ExceptionSorter、ValidConnectionChecker、JdbcDataSourceStat
这里重点关注ExceptionSorter和ValidConnectionChecker这两个类,这里会根据数据库类型进行选择。其中,ValidConnectionChecker用于对连接进行检测。
创建initialSize数量的连接
这里有两种方式创建连接,一种是异步,一种是同步。但是,根据我们的使用例子,createScheduler为null,所以采用的是同步的方式。
注意,后面的所有代码也是基于createScheduler为null来分析的。
创建logStatsThread、createConnectionThread和destroyConnectionThread
这里会启动三个线程。
等待
这里使用了CountDownLatch,保证当createConnectionThread和destroyConnectionThread开始run时再继续执行。
private final CountDownLatch initedLatch = new CountDownLatch(2);
// 线程进入等待,等待CreatorThread和DestroyThread执行
initedLatch.await();
我们进入到
DruidDataSource.CreateConnectionThread.run(),可以看到,一执行run方法就会调用countDown。destroyConnectionThread也是一样,这里就不放进来了。
注册MBean
接下来是注册MBean,会去注册
DruidDataSourceStatManager和DruidDataSource,启动我们的程度,通过jconsole就可以看到这两个MBean。JMX相关内容这里就不多扩展了,感兴趣的话可参考我的另一篇博客: 如何使用JMX来管理程序?
// 注册MBean,用于支持JMX
registerMbean();
通知createConnectionThread创建连接对象
前面已经讲过,当我们调用empty.signal(),会去唤醒处于empty.await()状态的CreateConnectionThread。CreateConnectionThread这个线只有在需要创建连接时才运行,否则会一直等待,后面会讲到。
连接对象的获取
了解下DruidPooledConnection这个类
用户调用
DruidDataSource.getConnection,拿到的对象是DruidPooledConnection,里面封装了DruidConnectionHolder,而这个对象包含了原生的连接对象和我们一开始创建的数据源对象。
DruidPooledConnection的UML图
概括下获取连接的过程
连接对象的获取过程可以概括为以下步骤:
- 初始化数据源(如果还没初始化);
- 获得连接对象,如果无可用连接,向createConnectionThread发送signal创建新连接,此时会进入等待;
- 如果设置了testOnBorrow,进行testOnBorrow检测,否则,如果设置了testWhileIdle,进行testWhileIdle检测;
- 如果设置了removeAbandoned,则会将连接对象放入activeConnections;
- 设置defaultAutoCommit,并返回;
- 执行filterChain。
初始化数据源的前面已经讲过了,这里就直接从第二步开始。
获取连接对象
进入
DruidDataSource.getConnectionInternal方法。除了获取连接对象,其他的大部分是校验和计数的内容。
下面再看下DruidDataSource.takeLast()方法(即没有配置maxWait时调用的方法)。该方法中,当没有空闲连接对象时,会尝试创建连接,此时该线程进入等待(notEmpty.await()),只有连接对象创建完成或池中回收了连接对象(notEmpty.signal()),该线程才会继续执行。
创建连接对象
前面已经讲到,创建连接是采用异步方式,进入到
DruidDataSource.CreateConnectionThread.run()。当不需要创建连接时,该线程进入empty.await()状态,此时需要用户线程调用empty.signal()来唤醒。
testOnBorrow或testWhileIdle
进入
DruidDataSource.getConnectionDirect(long)。该方法会使用到validConnectionChecker来校验连接的有效性。
removeAbandoned
进入
DruidDataSource.getConnectionDirect(long),这里不会进行检测,只是将连接对象放入activeConnections,具体泄露连接的检测工作是在DestroyConnectionThread线程中进行。
DestroyConnectionThread线程会根据我们设置的
timeBetweenEvictionRunsMillis来进行检验,具体的校验会去运行DestroyTask(DruidDataSource的内部类),这里看下DestroyTask的run方法。
进入
DruidDataSource.removeAbandoned(),当连接对象使用时间超过removeAbandonedTimeoutMillis,则会被丢弃掉。
执行filterChain
进入
DruidDataSource.getConnection。
进入到
FilterChainImpl.dataSource_connect。
这里以
StatFilter.dataSource_getConnection为例。
以上,druid的源码基本已经分析完,其他部分内容有空再做补充。
数据源的初始化
了解下DruidDataSource这个类
这里先来介绍下DruidDataSource这个类:
DruidDataSource的UML图
图中我只列出了几个重要的属性,这几个属性没有理解好,后面的源码很难看得进去。
概括下初始化的过程
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,用于支持JMX
- 如果设置了keepAlive,通知createConnectionThread创建连接对象
- 解锁
这个方法差不多200行,考虑篇幅,我删减了部分内容。
加锁和解锁
druid数据源初始化采用的是ReentrantLock,如下:
注意,以下步骤均在这个锁的范围内。
初始化属性
这部分内容主要是初始化一些属性,需要注意的一点就是,这里使用了AtomicLongFieldUpdater来进行原子更新,保证写的安全和读的高效,当然,还是cocurrent包的工具。
初始化过滤器
看到下面的代码会发现,我们还可以通过SPI机制来配置过滤器。
使用SPI配置过滤器时需要注意,对应的类需要加上@AutoLoad注解,另外还需要配置load.spifilter.skip=false,SPI相关内容可参考我的另一篇博客:使用SPI解耦你的实现类。
在这个方法里,主要就是初始化过滤器的一些属性而已。过滤器的部分,本文不会涉及到太多。
校验配置
这里只是简单的校验,不涉及太多复杂的逻辑。
初始化ExceptionSorter、ValidConnectionChecker、JdbcDataSourceStat
这里重点关注ExceptionSorter和ValidConnectionChecker这两个类,这里会根据数据库类型进行选择。其中,ValidConnectionChecker用于对连接进行检测。
创建initialSize数量的连接
这里有两种方式创建连接,一种是异步,一种是同步。但是,根据我们的使用例子,createScheduler为null,所以采用的是同步的方式。
注意,后面的所有代码也是基于createScheduler为null来分析的。
创建logStatsThread、createConnectionThread和destroyConnectionThread
这里会启动三个线程。
等待
这里使用了CountDownLatch,保证当createConnectionThread和destroyConnectionThread开始run时再继续执行。
private final CountDownLatch initedLatch = new CountDownLatch(2);
// 线程进入等待,等待CreatorThread和DestroyThread执行
initedLatch.await();
我们进入到
DruidDataSource.CreateConnectionThread.run(),可以看到,一执行run方法就会调用countDown。destroyConnectionThread也是一样,这里就不放进来了。
注册MBean
接下来是注册MBean,会去注册
DruidDataSourceStatManager和DruidDataSource,启动我们的程度,通过jconsole就可以看到这两个MBean。JMX相关内容这里就不多扩展了,感兴趣的话可参考我的另一篇博客: 如何使用JMX来管理程序?
// 注册MBean,用于支持JMX
registerMbean();
通知createConnectionThread创建连接对象
前面已经讲过,当我们调用empty.signal(),会去唤醒处于empty.await()状态的CreateConnectionThread。CreateConnectionThread这个线只有在需要创建连接时才运行,否则会一直等待,后面会讲到。
连接对象的获取
了解下DruidPooledConnection这个类
用户调用
DruidDataSource.getConnection,拿到的对象是DruidPooledConnection,里面封装了DruidConnectionHolder,而这个对象包含了原生的连接对象和我们一开始创建的数据源对象。
DruidPooledConnection的UML图
概括下获取连接的过程
连接对象的获取过程可以概括为以下步骤:
- 初始化数据源(如果还没初始化);
- 获得连接对象,如果无可用连接,向createConnectionThread发送signal创建新连接,此时会进入等待;
- 如果设置了testOnBorrow,进行testOnBorrow检测,否则,如果设置了testWhileIdle,进行testWhileIdle检测;
- 如果设置了removeAbandoned,则会将连接对象放入activeConnections;
- 设置defaultAutoCommit,并返回;
- 执行filterChain。
初始化数据源的前面已经讲过了,这里就直接从第二步开始。
获取连接对象
进入
DruidDataSource.getConnectionInternal方法。除了获取连接对象,其他的大部分是校验和计数的内容。
下面再看下DruidDataSource.takeLast()方法(即没有配置maxWait时调用的方法)。该方法中,当没有空闲连接对象时,会尝试创建连接,此时该线程进入等待(notEmpty.await()),只有连接对象创建完成或池中回收了连接对象(notEmpty.signal()),该线程才会继续执行。
创建连接对象
前面已经讲到,创建连接是采用异步方式,进入到
DruidDataSource.CreateConnectionThread.run()。当不需要创建连接时,该线程进入empty.await()状态,此时需要用户线程调用empty.signal()来唤醒。
testOnBorrow或testWhileIdle
进入
DruidDataSource.getConnectionDirect(long)。该方法会使用到validConnectionChecker来校验连接的有效性。
removeAbandoned
进入
DruidDataSource.getConnectionDirect(long),这里不会进行检测,只是将连接对象放入activeConnections,具体泄露连接的检测工作是在DestroyConnectionThread线程中进行。
DestroyConnectionThread线程会根据我们设置的
timeBetweenEvictionRunsMillis来进行检验,具体的校验会去运行DestroyTask(DruidDataSource的内部类),这里看下DestroyTask的run方法。
进入
DruidDataSource.removeAbandoned(),当连接对象使用时间超过removeAbandonedTimeoutMillis,则会被丢弃掉
执行filterChain
进入
DruidDataSource.getConnection。
进入到
FilterChainImpl.dataSource_connect。
这里以
StatFilter.dataSource_getConnection为例。
以上,druid的源码基本已经分析完,其他部分内容有空再做补充。