从源码分析DBCP数据库连接池的原理

背景

数据库连接池的概念大家应该都很熟悉,类似dbcp、c3p0,还有如今常用的druid等,使用时无非就是从网上抄几段配置,调一下参数,但是深入理解数据库连接池的原理在如今的Java开发中还是很有必要的,这里以dbcp为例,简要分析一下数据库连接池的实现原理

这里我采用的版本是2.5.0版本

	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-dbcp2</artifactId>
	    <version>2.5.0</version>
	</dependency>
复制代码

分析

准备

我们先来看一段原生jdbc代码(省略了异常捕获):

        Class.forName("com.mysql.cj.jdbc.Driver");
        String url = "jdbc:mysql://localhost:3306/xxxxxx";

        Connection conn = DriverManager.getConnection(url, "user", "password");
        Statement stat = conn.createStatement();
        ResultSet set = stat.executeQuery(sql);

        while(set.next()) {
            System.out.println(set.getString(2));
        }

        stat.close();
        conn.close();
复制代码

代码结构这里不关心,重点是其中的一句

		Connection conn = DriverManager.getConnection(url, "user", "password");
复制代码

为什么要单独拿这一条出来说呢?因为我们使用数据库连接池也是调用了getConnection方法,在JDBC 2.0 API中,Java提倡使用DateSource接口来获取连接

我们现在的重点就是找到dhcp提供的DataSource接口,如果我们使用过dhcp配置,就一定见过这样的配置

spring:
  datasource:
    type: org.apache.commons.dbcp2.BasicDataSource
复制代码

我们就以BasicDataSource作为突破点,这个类实现了DataSource接口:

public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBeanRegistration, AutoCloseable
复制代码

既然我们要探究原理,就不要关注细枝末节的部分,我们直接点进最核心的getConnection方法,同时为了代码结构清晰,这里忽略所有异常捕获语句:

    @Override
    public Connection getConnection() throws SQLException {
        if (Utils.IS_SECURITY_ENABLED) {
            final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
            return AccessController.doPrivileged(action);
        }
        return createDataSource().getConnection();
    }
复制代码

整段代码很简单,根据是否开启安全管理,来选择获取连接的方式,我们这里只看没有安全管理的方式

数据源的创建

我们先来看createDataSource方法:

    protected DataSource createDataSource() throws SQLException {
        if (closed) {
            throw new SQLException("Data source is closed");
        }
        // 如果数据源已存在,就直接返回
        if (dataSource != null) {
            return dataSource;
        }
        synchronized (this) {
        	// 使用双检锁的设计,保证dataSource是单例的
            if (dataSource != null) {
                return dataSource;
            }

			// 注册MBean,这里不是重点,可以暂时不关心
            jmxRegister();

            // 创建返回原生连接的工厂
            final ConnectionFactory driverConnectionFactory = createConnectionFactory();

            // 创建连接池工厂
            boolean success = false;
            PoolableConnectionFactory poolableConnectionFactory;
            try {
                poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory);
                poolableConnectionFactory.setPoolStatements(poolPreparedStatements);
                poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
                success = true;
            } catch (final SQLException se) {
                throw se;
            } catch (final RuntimeException rte) {
                throw rte;
            } catch (final Exception ex) {
                throw new SQLException("Error creating connection factory", ex);
            }

            if (success) {
                // 创建连接池成功
                createConnectionPool(poolableConnectionFactory);
            }

            // 创建数据源池来管理连接
            DataSource newDataSource;
            success = false;
            try {
                newDataSource = createDataSourceInstance();
                newDataSource.setLogWriter(logWriter);
                success = true;
            } catch (final SQLException se) {
                throw se;
            } catch (final RuntimeException rte) {
                throw rte;
            } catch (final Exception ex) {
                throw new SQLException("Error creating datasource", ex);
            } finally {
                if (!success) {
                    closeConnectionPool();
                }
            }

            // 如果初始化连接数大于0,就进行预加载
            try {
                for (int i = 0; i < initialSize; i++) {
                    connectionPool.addObject();
                }
            } catch (final Exception e) {
                closeConnectionPool();
                throw new SQLException("Error preloading the connection pool", e);
            }

            // 如果空间连接回收器执行间隔时间大于0,则添加回收器任务
            startPoolMaintenance();

			// 返回数据源
            dataSource = newDataSource;
            return dataSource;
        }
    }
复制代码

代码看似很长,其实流程很简单:

  1. 判断能不能直接返回结果(数据源关闭或已存在),如果能,就直接返回
  2. 创建返回数据库连接的工厂
  3. 创建连接池工厂
  4. 数据库连接工厂来包装连接池工厂
  5. 创建数据库连接池
  6. 通过连接池来创建数据源实例
  7. 根据设置的参数,判断是否要进行额外操作(比如预加载数据库连接)
  8. 返回数据源

我们重点只有三个:数据库连接工厂是什么,连接池工厂是什么,连接池是什么。我们先来看数据库连接工厂

createConnectionFactory

这个方法的代码相比于上面的又有些混乱了,为了助于理解,我这里还是忽略了所有的异常捕获代码段:

    protected ConnectionFactory createConnectionFactory() throws SQLException {
        // 加载JDBC驱动
        Driver driverToUse = this.driver;

        if (driverToUse == null) {
            Class<?> driverFromCCL = null;
            if (driverClassName != null) {
                if (driverClassLoader == null) {
                    driverFromCCL = Class.forName(driverClassName);
                } else {
                    driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
                }
            }

            if (driverFromCCL == null) {
            	// 这里的url就是我们与数据库建立连接的url
                driverToUse = DriverManager.getDriver(url);
            } else {
                driverToUse = (Driver) driverFromCCL.getConstructor().newInstance();
                if (!driverToUse.acceptsURL(url)) {
                    throw new SQLException("No suitable driver", "08001");
                }
            }
        }

        // 设置连接
        final String user = userName;
        if (user != null) {
            connectionProperties.put("user", user);
        } else {
            log("DBCP DataSource configured without a 'username'");
        }

        final String pwd = password;
        if (pwd != null) {
            connectionProperties.put("password", pwd);
        } else {
            log("DBCP DataSource configured without a 'password'");
        }

        final ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driverToUse, url,
                connectionProperties);
        return driverConnectionFactory;
    }
复制代码

这个方法做的事情很简单,就是单纯的加载驱动,真正创建连接工厂的是倒数第二行new DriverConnectionFactory这段代码,最终我们创建的也是DriverConnectionFactory类型对象,这里我们向这个工厂对象传入了数据库驱动、连接url、用户名和密码等属性

关于DriverConnectionFactory工厂类,这里就列一段其中的代码,我相信其他就不用多讲了

    @Override
    public Connection createConnection() throws SQLException {
        return driver.connect(connectionString, properties);
    }
复制代码
PoolableConnectionFactory

接下来看这个连接池工厂类,但是我们并不准备先研究这个类,而是回到createDataSource方法中,发现有一个createConnectionPool方法,传入了PoolableConnectionFactory类对象,我们进入这个方法中:

    protected void createConnectionPool(final PoolableConnectionFactory factory) {
        // 创建一个对象池
        final GenericObjectPoolConfig<PoolableConnection> config = new GenericObjectPoolConfig<>();
        
        updateJmxName(config);
        config.setJmxEnabled(registeredJmxObjectName != null);
        
        final GenericObjectPool<PoolableConnection> gop = createObjectPool(factory, config, abandonedConfig);
        gop.setMaxTotal(maxTotal);
        gop.setMaxIdle(maxIdle);
        gop.setMinIdle(minIdle);
        gop.setMaxWaitMillis(maxWaitMillis);
        gop.setTestOnCreate(testOnCreate);
        gop.setTestOnBorrow(testOnBorrow);
        gop.setTestOnReturn(testOnReturn);
        gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
        gop.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        gop.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
        gop.setTestWhileIdle(testWhileIdle);
        gop.setLifo(lifo);
        gop.setSwallowedExceptionListener(new SwallowedExceptionLogger(log, logExpiredConnections));
        gop.setEvictionPolicyClassName(evictionPolicyClassName);
        
        factory.setPool(gop);
        connectionPool = gop;
    }
复制代码
  1. 第一行创建了一个对象池,Apache提供了四种对象池,GenericObjectPoolConfig只是其中一种,我们在本篇中暂且不关心其实现细节,只需要明白它是一个对象池就够了
  2. 接下来两行与Jmx有关,这里暂且不关心
  3. 接下来数行,通过我们提供的连接池和其他配置来创建一个对象池,同时设置变量的值,这些值都可以通过我们的配置文件来设置
  4. 最后我们再把连接池设置到PoolableConnectionFactory工厂类中,并将数据源本身的连接池设置为该连接池

现在明白我们为什么不直接研究PoolableConnectionFactory了吧,因为实际上我们调用的连接池是connectionPool

获取连接

刚才我们介绍了了连接工厂、连接池工厂和连接池,为了防止你忘记接下来要干什么,这里再把我们一开始研究的代码段放上来:

		return createDataSource().getConnection();
复制代码

刚才只是研究了createDataSource方法,这个方法最终返回了一个数据源对象(这个数据源对象是通过connectionPool包装而来的PoolingDataSource<PoolableConnection>对象),我们接下来就要看真正获取连接的方法getConnection

这是一个抽象方法,因为createDataSource返回的是PoolingDataSource对象,所以我们就从这个对象中来分析,为了让代码结构清晰,这里依然略去了异常捕获代码:

    @Override
    public Connection getConnection() throws SQLException {
        final C conn = pool.borrowObject();
        if (conn == null) {
            return null;
        }
        return new PoolGuardConnectionWrapper<>(conn);
    }
复制代码

我们先解决两个可能会有的问题:

  • C是Connection子类
  • PoolGuardConnectionWrapper是为了确保被关闭的连接不能被重用的包装连接类

现在应该就没有问题了,整个流程就是获取连接,如果获取不到就返回空,否则返回一个包装类。连接通过borrowObject方法来获取,这个方法的实现是在GenericObjectPool中(不知道为什么是这个类的一定是前面的代码没有认真看),关于这个类的实现方法就不是本篇的重点,在以后我会单独写一篇文章来介绍Apache提供的四种对象池

总结

最后依照惯例,来总结一下DBCP连接池获取连接的步骤,这里以调用getConnection方法获取连接为例分析:

  1. 判断数据源是否关闭或已存在,如果关闭则抛出异常,如果已存在则直接返回
  2. 创建可以返回数据库连接的工厂类,并加载数据库驱动
  3. 通过数据库连接工厂类,来创建一个连接池工厂类
  4. 通过连接池工厂类,创建数据库连接池并设置属性
  5. 通过连接池来创建数据源,并根据配置的属性来进行一些初始化操作
  6. 通过数据源中的对象池来获取对象(连接)

转载于:https://juejin.im/post/5cf0756c6fb9a07f04202e32

猜你喜欢

转载自blog.csdn.net/weixin_34357267/article/details/91477689