druid 连接池源码分析(4)

427 阅读2分钟

今天在自己的democode中实现了 init driver class的方法

public class DataSource {


    protected Driver driver;
    protected volatile String                          driverClass;
    protected volatile ClassLoader                     driverClassLoader;
    protected volatile String                          jdbcUrl ;


    public  void resolveDriver( ) throws SQLException {
        if (this.driver == null) {
            if (this.driverClass == null || this.driverClass.isEmpty()) {
                this.driverClass = JdbcUtils.getDriverClassName(/*this.*/jdbcUrl);
             }
            //存在扩展任何driverclass-hive clickhouse

                if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) {
                    throw new SQLException("url not set");
                }
                driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
        } else {
            if (this.driverClass == null) {
                this.driverClass = driver.getClass().getName();
            }
        }
        System.out.println("driverClass"+driverClass.toString());
    }
}

简单的实现了resolveDriver这个方法,在之前略略看过的过程中,了解到这个方法是通过url来解析获得driver的类别

public static String getDriverClassName(String rawUrl) throws SQLException {
    if (rawUrl == null) {
        return null;
    }

    if (rawUrl.startsWith("jdbc:derby:")) {
        return "org.apache.derby.jdbc.EmbeddedDriver";
    } else if (rawUrl.startsWith("jdbc:mysql:")) {
        if (mysql_driver_version_6 == null) {
            mysql_driver_version_6 = loadClass("com.mysql.cj.jdbc.Driver") != null;
        }

        if (mysql_driver_version_6) {
            return MYSQL_DRIVER_6;
        } else {
            return MYSQL_DRIVER;
        }
    } else if (rawUrl.startsWith("jdbc:log4jdbc:")) {
        return LOG4JDBC_DRIVER;
    } else if (rawUrl.startsWith("jdbc:mariadb:")) {
        return MARIADB_DRIVER;
    } else if (rawUrl.startsWith("jdbc:oracle:") //
            || rawUrl.startsWith("JDBC:oracle:")) {
        return ORACLE_DRIVER;
    } else if (rawUrl.startsWith("jdbc:microsoft:")) {
        return "com.microsoft.jdbc.sqlserver.SQLServerDriver";
    }

如上述代码示例,方法通过url的格式获得了driver的具体方法,

public static Driver createDriver(ClassLoader classLoader, String driverClassName) throws SQLException {
    Class<?> clazz = null;
    if (classLoader != null) {
        try {
            clazz = classLoader.loadClass(driverClassName);
        } catch (ClassNotFoundException e) {
            // skip
        }
    }

    if (clazz == null) {
        try {
            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
            if (contextLoader != null) {
                clazz = contextLoader.loadClass(driverClassName);
            }
        } catch (ClassNotFoundException e) {
            // skip
        }
    }

    if (clazz == null) {
        try {
            clazz = Class.forName(driverClassName);
        } catch (ClassNotFoundException e) {
            throw new SQLException(e.getMessage(), e);
        }
    }

    try {
        return (Driver) clazz.newInstance();
    } catch (IllegalAccessException e) {
        throw new SQLException(e.getMessage(), e);
    } catch (InstantiationException e) {
        throw new SQLException(e.getMessage(), e);
    }
}

最后通过获得的类名称调用类加载器classloader返回具体的jdbc方法。

这样就完成了一个driver class的初始化

在这个过程中我们在constant中可以看到一个很有意思的类型枚举DbType

public enum DbType {
    other           (1 << 0),
    jtds            (1 << 1),
    hsql            (1 << 2),
    db2             (1 << 3),
    postgresql      (1 << 4),

    sqlserver       (1 << 5),
    oracle          (1 << 6),
    mysql           (1 << 7),
    mariadb         (1 << 8),
    derby           (1 << 9),

    hive            (1 << 10),
    h2              (1 << 11),
    dm              (1 << 12), // dm.jdbc.driver.DmDriver
    kingbase        (1 << 13),
    gbase           (1 << 14),

    oceanbase       (1 << 15),
    informix        (1 << 16),
    odps            (1 << 17),
    teradata        (1 << 18),
    phoenix         (1 << 19),

    edb             (1 << 20),
    kylin           (1 << 21), // org.apache.kylin.jdbc.Driver
    sqlite          (1 << 22),
    ads             (1 << 23),
    presto          (1 << 24),

    elastic_search  (1 << 25), // com.alibaba.xdriver.elastic.jdbc.ElasticDriver
    hbase           (1 << 26),
    drds            (1 << 27),


    ...
    ;

    public final long mask;
    public final long hashCode64;

    private DbType(long mask) {
        this.mask = mask;
        this.hashCode64 = FnvHash.hashCode64(name());
    }

    public static long of(DbType... types) {
        long value = 0;

        for (DbType type : types) {
            value |= type.mask;
        }

        return value;
    }

    public static DbType of(String name) {
        if (name == null || name.isEmpty()) {
            return null;
        }

        if ("aliyun_ads".equalsIgnoreCase(name)) {
            return ads;
        }

        try {
            return valueOf(name);
        } catch (Exception e) {
            return null;
        }
    }

    public final boolean equals(String other) {
        return this == of(other);
    }

在这个枚举中引用了 FNV哈希算法 FnvHash.hashCode64(name(), FNV哈希算法是一种高离散性的哈希算法,特别适用于哈希非常相似的字符串,例如:URL,IP,主机名,文件名等。

if (this.dbTypeName == null || this.dbTypeName.length() == 0) {
    this.dbTypeName = JdbcUtils.getDbType(jdbcUrl, null);
}
//获取DbType的方法
public static String getDbType(String rawUrl, String driverClassName) {
    DbType dbType = getDbTypeRaw(rawUrl, driverClassName);

    if (dbType == null) {
        return null;
    }

    return dbType.name();
}

这里的话会根据url获取dbtype ,一串经过fnvhash的字符串比如mysql就转换成为 (1 << 7) 而这个的用处是什么呢?

我们在写枚举参数时,可以用 | 运算符传入多个枚举值,比如:oracle | mysql。之所以可以这样写,是因为在定义枚举时提供了位运算。

将一个数字转换成二进制数,枚举中使用的是1,所以二进制数就是 0000 0001 ,将其转换成十进制数就是 1 * 2^0 + 0 * 2 ^ 1 + 0 * 2 ^ 2 + …… ,实际上就是 2^0 。

而位运算 1 << 7,就是 1000 0000 ,将1向左移动7位,其结果就是 0 * 2^0 + 0 * 2^1 +……+ 0 * 2 ^ 7,结果是240 。同理,其它几个结果也是这么运算的

这里的最后结果就是通过位运算的枚举把长段的url,直接转换为了一个数字,以降低整个运行过程中所用到的内存和加快运算的速度,

DbType dbType = DbType.of(this.dbTypeName);
if (dbType == DbType.mysql
        || dbType == DbType.mariadb
        || dbType == DbType.oceanbase
        || dbType == DbType.ads) {
    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");
    }
}

这一段就是位运算枚举转换以后的一段应用:利用位运算枚举版本的数据库类型dbType,判断是否要初始化cacheServerConfigurationSet这一个配置。

再往下看一个非常重要的方法

while (poolingCount < initialSize) {
    try {
        PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
        DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
        connections[poolingCount++] = holder;
    } catch (SQLException ex) {
        LOG.error("init datasource error, url: " + this.getUrl(), ex);
        if (initExceptionThrow) {
            connectError = ex;
            break;
        } else {
            Thread.sleep(3000);
        }
    }

首先统计了整个连接池中的资源poolingCount ,poolingCount是一个非常重要的计数器 可以看到获得这个数字的方法都是加锁的。

@Override
public int getPoolingCount() {
    lock.lock();
    try {
        return poolingCount;
    } finally {
        lock.unlock();
    }
}

如果获得的poolingCount最后是小于设置的最大数量这里就开始获取物理连接了。

从上可以看出,druid获取数据库连接首先也是要获取到 driver 然后再调用它的 connect 方法。 上面代码的主要逻辑也还是先实例化 driver, 然后通过 driver 获取一个数据库连接,这种方式和jdbc是一致的,只不过driud帮我们做了这件事情,我们不用再自己做了而已。并且DruidDataSource 在获取到连接之后,会把连接存在池中,以达到我们每次请求都可以直接从池中分配连接,而不是自己再去重新初始化的一个作用。

接下来是一个死循环的创建:

初始化创建和销毁线程,即调起这个线程进行各自工作,里面通过锁控制

protected void createAndStartCreatorThread() {
    if (createScheduler == null) {
        String threadName = "Druid-ConnectionPool-Create-" + System.identityHashCode(this);
        createConnectionThread = new CreateConnectionThread(threadName);
        createConnectionThread.start();
        return;
    }

    initedLatch.countDown();
}

createAndStartCreatorThread里面调用了CreateConnectionThread 在这个thread中还同时调用了关闭线程
首先线程启动的时候 调用initedLatch.countDown(),这是一个 CountDownLatch来控制整个线程池初始化的进程

public class CreateConnectionThread extends Thread {

    public CreateConnectionThread(String name){
        super(name);
        this.setDaemon(true);
    }

    public void run() {
        initedLatch.countDown();

long discardCount = DruidDataSource.this.discardCount;
boolean discardChanged = discardCount - lastDiscardCount > 0;

接下来是连接池创建的条件emptyWait,如果有异常,或者诸如 poolingCount == 0并且(连接数为空了)并且discardChanged 并没有改变 等等等。程序就会在empty条件上等待创建,就是等emptyWait这个条件成立再运行,并且在创建的时候还会再次判断不超过maxActive数量的连接

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()
    ) {
        empty.await();
    }

    // 防止创建超过maxActive数量的连接
    if (activeCount + poolingCount >= maxActive) {
        empty.await();
        continue;
    }
}

在线程创建的最后是一个死循环: 如果没创建连接,继续,如果连接关闭成功,跳出循环 直觉上这可能会导致一些bug...

if (connection == null) {
    continue;
}

boolean result = put(connection);
if (!result) {
    JdbcUtils.close(connection.getPhysicalConnection());
    LOG.info("put physical connection to pool failed.");
}

errorCount = 0; // reset errorCount

if (closing || closed) {
    break;
}

创建线程的过程就讲到这里,明天我们再看一下销毁线程的部分代码。