连接池
前言
druid是阿里爸爸的开源数据库连接池,据说其性能算是位于领先的水平,从连接的创建和销毁这个性能方面优于其它连接池,但是觉得和hikariCP,的速度比起来还是差点。但是两者各有好处,一个是扩展性比较优秀功能比较全,一个是速度比较块。以下是性能对照图:
图片出处:
https://github.com/brettwooldridge/HikariCP
图片出处:
druid Github
图片出处:
http://blog.didispace.com/java-datasource-pool-compare/
于是便引起了我这个菜鸟的好奇心,由于druid功能强大,我是菜鸟,就看看连接池的一些内容吧。
小试牛刀-使用
配置:
配置列表,其兼容DBCP的各项配置,但是有些许的语义区别。以下是我简单的入门配置。
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/day14_customer
username=root
password=
#初始链接数,在连接池被创建的时候初始化的连接数
initSize=20
#最大连接池数量
maxActive=20
#最小连接池数量
Minidle=5
#超时等待时间
maxWait=60000
#指定连接属性
connectionProperties=useSSL=true;rewriteBATchedstatements=true
在java中简单使用:
package Utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class Druid {
private static DataSource dataSource;
static {
try {
InputStream inputStream = DBCP.class.getClassLoader().getResourceAsStream("dbconfig.properties");
Properties properties = new Properties();
properties.load(inputStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printstacktrace();
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void release(Connection conn, Statement st, ResultSet rs) {
if (conn != null) {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
挺简单的没啥好说的
看看源码
重要变量
DruidAbstractDataSource.class
//重入锁
protected ReentrantLock lock;
//empty,notEmpty条件,负责线程的阻塞唤醒
protected Condition notEmpty;
protected Condition empty;
DruidSource.class
//存放线程的底层数组
private volatile DruidConnectionHolder[] connections;
工厂函数创建datasource
public static DataSource createDataSource(Map properties) throws Exception {
DruidDataSource dataSource = new DruidDataSource();
//从properties中配置datasource
config(dataSource, properties);
return dataSource;
}
在获取连接的过程进行初始化
public DruidpooledConnection getConnection(long maxWaitMillis) throws SQLException {
this.init();
if (this.filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
return this.getConnectionDirect(maxWaitMillis);
}
}
初始化的方法中有这样的几个线程启动,一个是create一个是destroy
this.createAndLogThread();
this.createAndStartCreatorThread();
this.createAndStartDestroyThread();
lock、condition、两个线程-创建/销毁connection
从源码我们可以看出druid连接池是基于ReetrantLock的,它有两个condition,empty和notEmpty,一个是监控连接池是否为空一个是监控连接池不为空的。这是连接池的核心部分,让我们一探究竟。注意,这两个条件condition都是谁用?等待为空的线程是创建condition的线程,等待不为空的是getConnection。
创建线程:
com.alibaba.druid.pool.DruidDataSource.CreateConnectionThread
try {
//等待为空的变量,依据这个变量来进行condition:empty的阻塞(awiait)
boolean emptyWait = true;
if (DruidDataSource.this.createERROR != null && DruidDataSource.this.poolingCount == 0 && !discardChanged) {
emptyWait = false;
}
if (emptyWait && DruidDataSource.this.asyncInit && DruidDataSource.this.createCount < (long)DruidDataSource.this.initialSize) {
emptyWait = false;
}
if (emptyWait) {
if (DruidDataSource.this.poolingCount >= DruidDataSource.this.notEmptyWaitThreadCount && (!DruidDataSource.this.keepAlive || DruidDataSource.this.activeCount + DruidDataSource.this.poolingCount >= DruidDataSource.this.minIdle)) {
DruidDataSource.this.empty.await();
}
if (DruidDataSource.this.activeCount + DruidDataSource.this.poolingCount >= DruidDataSource.this.maxActive) {
DruidDataSource.this.empty.await();
continue;
}
}
等待空的两个if条件
- 池中的连接数量>=等待不为空的线程数量.意思就是说你请求一个连接我连接池里面有,这个时候就不用创建连接了,连接的线程就await,阻塞
- 活跃的数量+池中的数量>=连接池总数量,意思就是说连接池数量达到了池的上限,这个时候就不能再创建连接了。
if (connection != null) {
// 往线程中put进一个连接connection
boolean result = DruidDataSource.this.put(connection);
if (!result) {
JdbcUtils.close(connection.getPhysicalConnection());
DruidDataSource.LOG.info("put physical connection to pool failed.");
}
errorCount = 0;
}
获取线程
获取线程的逻辑主要是它来实现的,让我们进一步探究一下。
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
this.init();
if (this.filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
return this.getConnectionDirect(maxWaitMillis);
}
}
最后的弹出连接是这个方法:
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
while(this.poolingCount == 0) {
//连接池空了,唤醒生产者来创建连接
this.emptysignal();
if (this.failFast && this.failContinuous.get()) {
throw new DataSourceNotAvailableException(this.createError);
}
++this.notEmptyWaitThreadCount;
if (this.notEmptyWaitThreadCount > this.notEmptyWaitThreadPeak) {
this.notEmptyWaitThreadPeak = this.notEmptyWaitThreadCount;
}
//阻塞了一个,数量也就-1
try {
this.notEmpty.await();
} finally {
--this.notEmptyWaitThreadCount;
}
++this.notEmptyWaitCount;
if (!this.enable) {
connectErrorCountUpdater.incrementAndGet(this);
throw new DataSourcedisableException();
}
}
} catch (InterruptedException var5) {
this.notEmpty.signal();
++this.notEmptySignalCount;
throw var5;
}
//减少池中的连接计数,下面的部分就是取连接的操作
this.decrementPoolingCount();
DruidConnectionHolder last = this.connections[this.poolingCount];
this.connections[this.poolingCount] = null;
return last;
}
两个有用的字段:notEmptyWaitThreadCount这个字段也就是消费者数量,notEmptyWaitCount维护的就是正在等待的消费者数量
这让我就突然想起了生产者和消费者的思想,创建线程是生产者,获取线程是消费者,不难发现notEmpty这个Condition是用在了它上面。
销毁线程
public class DestroyTask implements Runnable {
public DestroyTask() {
}
public void run() {
DruidDataSource.this.shrink(true, DruidDataSource.this.keepAlive);
if (DruidDataSource.this.isRemoveAbandoned()) {
DruidDataSource.this.removeAbandoned();
}
}
}
shrink的关键就在这,其主要功能是收缩就是空闲的太多了要进行回收了。
if (evictCount > 0) {
for(checkCount = 0; checkCount < evictCount; ++checkCount) {
holer = this.evictConnections[checkCount];
connection = holer.getConnection();
JdbcUtils.close(connection);
destroyCountUpdater.incrementAndGet(this);
}
Arrays.fill(this.evictConnections, (Object)null);
}
总结
这次的小blog只是就简单的实现原理看了看,可以看出连接池是生产者消费者这种形式运作的,主要是两个线程来保证连接池,其中很多细节未涉及,笔者能力有限。如果有机会还想学习一下监控,据说是通过责任链来实现的,责任链我不懂,就没有深入。总的来说,旨在入个门把。
深入学习的参考链接
性能分析对比:
http://developer.51cto.com/art/201807/579402.htm
源码:
https://www.jianshu.com/p/549bfacebb38
https://www.jianshu.com/p/16a646a4eccd
配置表:
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
相关阅读
springboot 2.0 使用Hikari连接池(号称java平台最快的,
1.springboot 2.0 默认连接池就是Hikari了,所以引用parents后不用专门加依赖 2.贴我自己的配置(时间单位都是毫秒) # jdbc_config
<!--acquireIncrement:链接用完了自动增量3个。 --> <property name="acquireIncrement">3</property> <!--acquireR