Druid源码阅读4-DruidDataSource的getConnection过程

DruidDataSource连接池实现了javaX.sql包中,DataSource接口的全部方法。getConnection也来自于javaX.sql.DataSource接口。
而DruidPooledConnection也实现了接口java.sql.Connection。
这样就能在各种场景中通过这个接口来获取数据库连接。

1.fileter处理--责任链模式

在执行getConnection方法的过程中,首先确认DataSource是否完成了初始化。由于 init方法采用了Double Check机制,如果初始化完成则不会再次执行,因此这里不会造成系统多次初始化。

public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
    //调用初始化,以避免在获取连接的时候DruidDataSource的初始化工作还没完成。
    init();
    
    //这里有两个分支,判断filters是否存在过滤器,如果存在则先执行过滤器中的内容,这采用责任链模式实现。
    if (filters.size() > 0) {
        //责任链执行过程
        FilterChainImpl filterChain = new FilterChainImpl(this);
        return filterChain.dataSource_connect(this, maxWaitMillis);
    } else {
        //直接创建连接
        return getConnectionDirect(maxWaitMillis);
    }
}

这个filter的处理过程是一个经典的责任链模式。
new了一个FilterChainImpl对象,而这个对象的构造函数 this 。
查看了一下,DruidDataSource的父类DruidAbstractDataSource正好实现了DataSourceProxy接口,也就是说,DruidDataSource本身就是一个DataSourceProxy。
这样做的好处是,FilterChainImpl本身不用维护任何存放filters的数组,这个数组可以直接复用DruidDataSource中的数据结构。
在FilterChainImpl中:


public FilterChainImpl(DataSourceProxy dataSource){
    this.dataSource = dataSource;
    this.filterSize = getFilters().size();
}

public FilterChainImpl(DataSourceProxy dataSource, int pos){
    this.dataSource = dataSource;
    this.pos = pos;
    this.filterSize = getFilters().size();
}

public List<Filter> getFilters() {
    return dataSource.getProxyFilters();
}

private Filter nextFilter() {
    return getFilters()
            .get(pos++);
}

在DruidAbstractDataSource中,这个filters的数据结构:

protected List<Filter>  filters = new CopyOnWriteArrayList<Filter>();

这样所有的filters都将存放到责怪list中。
再查看 FilterChainImpl的dataSource_connect方法:

@Override
public DruidPooledConnection dataSource_connect(DruidDataSource dataSource, long maxWaitMillis) throws SQLException {
    //判断当前filter的指针是否小于filterSize的大小,如果小于,则执行filter的dataSource_getConnection
    if (this.pos < filterSize) {
        DruidPooledConnection conn = nextFilter().dataSource_getConnection(this, dataSource, maxWaitMillis);
        return conn;
    }
    //反之 调用getConnectionDirect 创建数据库连接。
    return dataSource.getConnectionDirect(maxWaitMillis);
}

这样看上去将调用filter的dataSource_getConnection方法。
但是这个地方实际上涉及比较巧妙,采用了一个父类FilterAdapter,所有的Filter都集成这个父类FilterAdapter,而父类本身又实现了Filter接口,本身是一个Filter.
StartFilter等Filter的实现类,没有实现dataSource_getConnection方法。
因此这个方法实际上执行的逻辑就是FilterAdapter类的dataSource_getConnection方法。

@Override
public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource,
                                                      long maxWaitMillis) throws SQLException {
    return chain.dataSource_connect(dataSource, maxWaitMillis);
}

此时调用dataSource_connect之后,又回到了FilterChainImpl的dataSource_connect方法中。
不过此时pos会增加,if判断中的逻辑不会执行。那么就会执行 dataSource.getConnectionDirect(maxWaitMillis);直接创建一个连接之后返回。
这就是getConnection过程中处理filter的责任链模式,这也是我们在编码的过程中值得借鉴的地方。
在getConnection中,无论是否存在filter,那么最终将通过getConnectionDirect来创建连接。getConnectionDirect才是连接被创建的最终方法。

2.getConnectionDirect

getConnectionDirect方法也不是最终创建数据库连接的方法。
这个方法会通过一个for循环自旋,确保连接的创建。
在GetConnectionTimeoutException异常处理中,这个地方有一个重试次数notFullTimeoutRetryCount,每次重试的时间为maxWaitMillis。

int notFullTimeoutRetryCnt = 0;
//自旋
for (;;) {
    // handle notFullTimeoutRetry
    DruidPooledConnection poolableConnection;
    try {
    //调用getConnectionInternal 获取连接
        poolableConnection = getConnectionInternal(maxWaitMillis);
    } catch (GetConnectionTimeoutException ex) {
    //超时异常处理,判断是否达到最大重试次数 且连接池是否已满
        if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
            notFullTimeoutRetryCnt++;
            //日志打印
            if (LOG.isWarnEnabled()) {
                LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
            }
            continue;
        }
        throw ex;
    }
    //后续代码略
    ... ...
}

通过自旋的方式确保获取到连接。之后对获取到的连接进行检测,主要的检测参数有:

参数 说明
testOnBorrow 默认值通常为false,用在获取连接的时候执行validationQuery检测连接是否有效。这个配置会降低性能。
testOnReturn 默认值通常为false,用在归还连接的时候执行validationQuery检测连接是否有效,这个配置会降低性能。
testWhileIdle 这个值通常建议为true,连接空闲时间大于timeBetweenEvictionRunsMillis指定的毫秒,就会执行参数validationQuery指定的SQL来检测连接是否有效。这个参数会定期执行。
validationQuery 用来检测连接是否有效的sql,如果validationQuery为空,那么testOnBorrow、testOnReturn、testWhileIdle这三个参数都不会起作用,配置参考:validationQuery=SELECT 1

在getConnection中,将会发生的检测过程伪代码:

if (testOnBorrow){
  //获取连接时检测
}else {
    if (poolableConnection.conn.isClosed()) {
      //检测连接是否关闭
    }
    
     if (testWhileIdle) {
       //空闲检测 
     }

}

上述检测过程都会调用testConnectionInternal(poolableConnection.holder, poolableConnection.conn);进行检测。

此外还有一个很重要的参数removeAbandoned。这个参数相关的配置参数有:

参数 说明
removeAbandoned 如果连接泄露,是否需要回收泄露的连接,默认false;
logAbandoned 如果回收了泄露的连接,是否要打印一条log,默认false;
removeAbandonedTimeoutMillis 连接回收的超时时间,默认5分钟;

参数removeAbandoned的作用在于,如果有线程从Druid中获取到了连接并没有及时归还,那么Druid就会定期检测该连接是否会处于运行状态,如果不处于运行状态,则被获取时间超过removeAbandonedTimeoutMillis就会强制回收该连接。
这个检测的过程是在回收线程中完成的,在getConnection的过程中,只是判断该参数是否被设置,然后加上对应的标识。

if (removeAbandoned) {
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    //设置 stackTrace
    poolableConnection.connectStackTrace = stackTrace;
    //设置setConnectedTimeNano
    poolableConnection.setConnectedTimeNano();
    //打开traceEnable
    poolableConnection.traceEnable = true;

    activeConnectionLock.lock();
    try {
        activeConnections.put(poolableConnection, PRESENT);
    } finally {
        activeConnectionLock.unlock();
    }
}

最后还需要对defaultAutoCommit参数进行处理:

if (!this.defaultAutoCommit) {
    poolableConnection.setAutoCommit(false);
}

至此,一个getConnetion方法创建完毕。

3.getConnectionInternal

getConnectionInternal方法中创建连接:
首先判断连接池状态 closed 和enable状态是否正确,如果不正确则抛出异常退出。

之后的逻辑:

 for (boolean createDirect = false;;){
    if(createDirect){
        //直接创建连接的逻辑
    }
    
    if (maxWaitThreadCount > 0
        && notEmptyWaitThreadCount >= maxWaitThreadCount) {
        // 判断最大等待线程数如果大于0且notEmpty上的等待线程超过了这个值 那么抛出异常
        
        }

    //其他相关参数检测 抛出异常
    
    //判断如果createScheduler存在,且executor.getQueue().size()大于0 那么将启用createDirect逻辑,退出本持循环
    if (createScheduler != null
        && poolingCount == 0
        && activeCount < maxActive
        && creatingCountUpdater.get(this) == 0
        && createScheduler instanceof ScheduledThreadPoolExecutor) {
    ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
    if (executor.getQueue().size() > 0) {
        createDirect = true;
        continue;
    }
    }
    
    //如果maxWait大于0,调用 pollLast(nanos),反之则调用takeLast()
    //获取连接的核心逻辑
    if (maxWait > 0) {
        holder = pollLast(nanos);
    } else {
        holder = takeLast();
    }

}

getConnectionInternal 方法内部存在一大堆参数检测功能,根据一系列参数判断,是否需要直接创建一个连接。
反之,则调用pollLast 或 takeLast 方法。这两个方法如果获取不到连接,将会wait,之后通知生产者线程创建连接。
这个地方有一个风险就是,如果仅仅采用单线程的方式创建连接,一旦生产者线程由于其他原因阻塞,那么getConnection将会产被长时间阻塞。

之后获得holder之后,通过holder产生连接。

holder.incrementUseCount();
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);

4.pollLast 与 takeLast

polllast的方法核心逻辑是自旋,判断是否有可用连接,然后发送empty消息,通知生产者线程可以创建连接。之后阻塞wait。只不过这个方法需要设置超时时间。

for (;;) {
        //如果没有可用的连接
        if (poolingCount == 0) {
         emptySignal(); // send signal to CreateThread create connection

           estimate = notEmpty.awaitNanos(estimate); // signal by
           
         }
         //之后获取最后一个连接
           DruidConnectionHolder last = connections[poolingCount];
}       

而takeLast方法与pollLast方法类似,只是等待的过程中,不增加超时时间,一直等到生产者的通知为止。

 while (poolingCount == 0) {
 
    emptySignal(); // send signal to CreateThread create connection
     
    try {
        notEmpty.await(); // signal by recycle or creator
    } finally {
        notEmptyWaitThreadCount--;
    }
    
 }
decrementPoolingCount();
//最后获取数组中的最后一个连接。
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;

至此,getConnection的过程大致分析完毕。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容