前景回顾

第一节 从零开始手写 mybatis(一)MVP 版本 中咱们实现了一个最根本的能够运行的 mybatis。

第二节 从零开始手写 mybatis(二)mybatis interceptor 插件机制详解

本节咱们一起来看一下如何实现一个数据库连接池。

为什么须要连接池?

数据库连贯的创立是十分耗时的一个操作,在高并发的场景,如果每次对于数据库的拜访都从新创立的话,老本太高。

于是就有了“池化”这种解决方案。

这种计划在咱们日常生活中也是亘古未有,比方资金池,需要池,乃至人力资源池。

思维都是共通的。

咱们本节一起来从零实现一个繁难版本的数据库连接池,不过麻雀虽小,五脏俱全。

将从以下几个方面来开展:

(1)一般的数据库连贯创立

(2)主动适配 jdbc 驱动

(3)指定大小的连接池创立

(4)获取连贯时增加超时检测

(5)增加对于连贯有效性的检测

一般的数据库连贯创立

这种就是最一般的不实用池化的实现。

实现

mybatis 默认其实也是这种实现,不过咱们在这个根底上做了一点优化,那就是能够依据 url 主动适配 driverClass。

public class UnPooledDataSource extends AbstractDataSourceConfig {    @Override    public Connection getConnection() throws SQLException {        DriverClassUtil.loadDriverClass(super.driverClass, super.jdbcUrl);        return DriverManager.getConnection(super.getJdbcUrl(),                super.getUser(), super.getPassword());    }}

主动适配

这个个性次要是参考阿里的 druid 连接池实现,在用户没有指定驱动类时,主动适配。

外围代码如下:

/** * 加载驱动类信息 * @param driverClass 驱动类 * @param url 连贯信息 * @since 1.2.0 */public static void loadDriverClass(String driverClass, final String url) {    ArgUtil.notEmpty(url, url);    if(StringUtil.isEmptyTrim(driverClass)) {        driverClass = getDriverClassByUrl(url);    }    try {        Class.forName(driverClass);    } catch (ClassNotFoundException e) {        throw new JdbcPoolException(e);    }}

如何依据 url 获取启动类呢?实际上就是一个 map 映射。

/** * 依据 URL 获取对应的驱动类 * * 1. 禁止 url 为空 * 2. 如果未找到,则间接报错。 * @param url url * @return 驱动信息 */private static String getDriverClassByUrl(final String url) {    ArgUtil.notEmpty(url, "url");    for(Map.Entry<String, String> entry : DRIVER_CLASS_MAP.entrySet()) {        String urlPrefix = entry.getKey();        if(url.startsWith(urlPrefix)) {            return entry.getValue();        }    }    throw new JdbcPoolException("Can't auto find match driver class for url: " + url);}

其中 DRIVER_CLASS_MAP 映射如下:

url 前缀驱动类
jdbc:sqliteorg.sqlite.JDBC
jdbc:derbyorg.apache.derby.jdbc.EmbeddedDriver
jdbc:edbcca.edbc.jdbc.EdbcDriver
jdbc:ingrescom.ingres.jdbc.IngresDriver
jdbc:hsqldborg.hsqldb.jdbcDriver
jdbc:JSQLConnectcom.jnetdirect.jsql.JSQLDriver
jdbc:sybase:Tdscom.sybase.jdbc2.jdbc.SybDriver
jdbc:firebirdsqlorg.firebirdsql.jdbc.FBDriver
jdbc:microsoftcom.microsoft.jdbc.sqlserver.SQLServerDriver
jdbc:mckoicom.mckoi.JDBCDriver
jdbc:oracleoracle.jdbc.driver.OracleDriver
jdbc:as400com.ibm.as400.access.AS400JDBCDriver
jdbc:fakecom.alibaba.druid.mock.MockDriver
jdbc:pointbasecom.pointbase.jdbc.jdbcUniversalDriver
jdbc:sapdbcom.sap.dbtech.jdbc.DriverSapDB
jdbc:postgresqlorg.postgresql.Driver
jdbc:cloudscapeCOM.cloudscape.core.JDBCDriver
jdbc:timestencom.timesten.jdbc.TimesTenDriver
jdbc:h2org.h2.Driver
jdbc:jtdsnet.sourceforge.jtds.jdbc.Driver
jdbc:odpscom.aliyun.odps.jdbc.OdpsDriver
jdbc:db2COM.ibm.db2.jdbc.app.DB2Driver
jdbc:mysqlcom.mysql.jdbc.Driver
jdbc:informix-sqlicom.informix.jdbc.IfxDriver
jdbc:mockcom.alibaba.druid.mock.MockDriver
jdbc:mimer:multi1com.mimer.jdbc.Driver
jdbc:interbaseinterbase.interclient.Driver
jdbc:JTurbocom.newatlanta.jturbo.driver.Driver

池化实现

接下来咱们依据指定的大小创立一个初始化的连接池。

定义池化的相干信息

咱们首先定义一个接口:

/** * 池化的连接池 * @since 1.1.0 */public interface IPooledConnection extends Connection {    /**     * 是否忙碌     * @since 1.1.0     * @return 状态     */    boolean isBusy();    /**     * 设置状态     * @param busy 状态     * @since 1.1.0     */    void setBusy(boolean busy);    /**     * 获取真正的连贯     * @return 连贯     * @since 1.1.0     */    Connection getConnection();    /**     * 设置连贯信息     * @param connection 连贯信息     * @since 1.1.0     */    void setConnection(Connection connection);    /**     * 设置对应的数据源     * @param dataSource 数据源     * @since 1.5.0     */    void setDataSource(final IPooledDataSourceConfig dataSource);    /**     * 获取对应的数据源信息     * @return 数据源     * @since 1.5.0     */    IPooledDataSourceConfig getDataSource();}

这里咱们间接继承了 Connection 接口,实现时全副对 Connection 做一个代理。

内容较多,然而比较简单,此处不再赘述。

连接池初始化

依据配置初始化大小:

/** * 初始化连接池 * @since 1.1.0 */private void initJdbcPool() {    final int minSize = super.minSize;    pool = new ArrayList<>(minSize);    for(int i = 0; i < minSize; i++) {        IPooledConnection pooledConnection = createPooledConnection();        pool.add(pooledConnection);    }}

createPooledConnection 内容如下:

/** * 创立一个池化的连贯 * @return 连贯 * @since 1.1.0 */private IPooledConnection createPooledConnection() {    Connection connection = createConnection();    IPooledConnection pooledConnection = new PooledConnection();    pooledConnection.setBusy(false);    pooledConnection.setConnection(connection);    pooledConnection.setDataSource(this);    return pooledConnection;}

咱们应用 busy 属性,来标识以后连贯是否可用。

新创建的连贯默认都是可用的。

连贯的获取

整体流程如下:

(1)池中有连贯,间接获取

(2)池中没有连贯,且没达到最大的大小,能够创立一个,而后返回

(3)池中没有连贯,然而曾经达到最大,则进行期待。

@Overridepublic synchronized Connection getConnection() throws SQLException {    //1. 获取第一个不是 busy 的连贯    Optional<IPooledConnection> connectionOptional = getFreeConnectionFromPool();    if(connectionOptional.isPresent()) {        return connectionOptional.get();    }    //2. 思考是否能够扩容    if(pool.size() >= maxSize) {        //2.1 立即返回        if(maxWaitMills <= 0) {            throw new JdbcPoolException("Can't get connection from pool!");        }        //2.2 循环期待        final long startWaitMills = System.currentTimeMillis();        final long endWaitMills = startWaitMills + maxWaitMills;        while (System.currentTimeMillis() < endWaitMills) {            Optional<IPooledConnection> optional = getFreeConnectionFromPool();            if(optional.isPresent()) {                return optional.get();            }            DateUtil.sleep(1);            LOG.debug("期待连接池偿还,wait for 1 mills");        }        //2.3 期待超时        throw new JdbcPoolException("Can't get connection from pool, wait time out for mills: " + maxWaitMills);    }    //3. 扩容(临时只扩容一个)    LOG.debug("开始扩容连接池大小,step: 1");    IPooledConnection pooledConnection = createPooledConnection();    pooledConnection.setBusy(true);    this.pool.add(pooledConnection);    LOG.debug("从扩容后的连接池中获取连贯");    return pooledConnection;}

getFreeConnectionFromPool() 外围代码如下:

间接获取一个不是忙碌状态的连贯即可。

/** * 获取闲暇的连贯 * @return 连贯 * @since 1.3.0 */private Optional<IPooledConnection> getFreeConnectionFromPool() {    for(IPooledConnection pc : pool) {        if(!pc.isBusy()) {            pc.setBusy(true);            LOG.debug("从连接池中获取连贯");            return Optional.of(pc);        }    }    // 空    return Optional.empty();}

连贯的偿还

以前 connection 的偿还是间接将连贯敞开,这里咱们做了一个重载。

只是调整下对应的状态即可。

@Overridepublic void returnConnection(IPooledConnection pooledConnection) {    // 验证状态    if(testOnReturn) {        checkValid(pooledConnection);    }    // 设置为不忙碌    pooledConnection.setBusy(false);    LOG.debug("偿还连贯,状态设置为不忙碌");}

连贯的有效性

池中的连贯存在有效的可能,所以须要咱们对其进行定期的检测。

配置解说

验证的机会是一门学识,咱们能够在获取时检测,能够在偿还时检测,然而二者都比拟耗费性能。

比拟好的形式是在闲暇的时候进行校验。

配置次要参考 druid 的配置,对应的接口如下:

/** * 设置验证查问的语句 * * 如果这个值为空,那么 {@link #setTestOnBorrow(boolean)} * {@link #setTestOnIdle(boolean)}} * {@link #setTestOnReturn(boolean)} * 都将有效 * @param validQuery 验证查问的语句 * @since 1.5.0 */void setValidQuery(final String validQuery);/** * 验证的超时秒数 * @param validTimeOutSeconds 验证的超时秒数 * @since 1.5.0 */void setValidTimeOutSeconds(final int validTimeOutSeconds);/** * 获取连贯时进行校验 * * 备注:影响性能 * @param testOnBorrow 是否 * @since 1.5.0 */void setTestOnBorrow(final boolean testOnBorrow);/** * 偿还连贯时进行校验 * * 备注:影响性能 * @param testOnReturn 偿还连贯时进行校验 * @since 1.5.0 */void setTestOnReturn(final boolean testOnReturn);/** * 空闲的时候进行校验 * @param testOnIdle 空闲的时候进行校验 * @since 1.5.0 */void setTestOnIdle(final boolean testOnIdle);/** * 空闲时进行校验的工夫距离 * @param testOnIdleIntervalSeconds 工夫距离 * @since 1.5.0 */void setTestOnIdleIntervalSeconds(final long testOnIdleIntervalSeconds);

约定优于配置

所有的属性都反对用户自定义,以满足不同的利用场景。

同时也秉承着默认的配置就是最罕用的配置,默认的配置如下:

/** * 默认验证查问的语句 * @since 1.5.0 */public static final String DEFAULT_VALID_QUERY = "select 1 from dual";/** * 默认的验证的超时工夫 * @since 1.5.0 */public static final int DEFAULT_VALID_TIME_OUT_SECONDS = 5;/** * 获取连贯时,默认不校验 * @since 1.5.0 */public static final boolean DEFAULT_TEST_ON_BORROW = false;/** * 偿还连贯时,默认不校验 * @since 1.5.0 */public static final boolean DEFAULT_TEST_ON_RETURN = false;/** * 默认空闲的时候,进行校验 * * @since 1.5.0 */public static final boolean DEFAULT_TEST_ON_IDLE = true;/** * 1min 主动校验一次 * * @since 1.5.0 */public static final long DEFAULT_TEST_ON_IDLE_INTERVAL_SECONDS = 60;

检测的实现

这里我参考了一篇 statckOverflow 的文章,其实还是应用 Connection#isValid 验证比较简单。

/** * https://stackoverflow.com/questions/3668506/efficient-sql-test-query-or-validation-query-that-will-work-across-all-or-most * * 真正反对规范的,间接应用 {@link Connection#isValid(int)} 验证比拟适合 * @param pooledConnection 连接池信息 * @since 1.5.0 */private void checkValid(final IPooledConnection pooledConnection) {    if(StringUtil.isNotEmpty(super.validQuery)) {        Connection connection = pooledConnection.getConnection();        try {            // 如果连贯有效,从新申请一个新的代替            if(!connection.isValid(super.validTimeOutSeconds)) {                LOG.debug("Old connection is inValid, start create one for it.");                Connection newConnection = createConnection();                pooledConnection.setConnection(newConnection);                LOG.debug("Old connection is inValid, finish create one for it.");            }        } catch (SQLException throwables) {            throw new JdbcPoolException(throwables);        }    } else {        LOG.debug("valid query is empty, ignore valid.");    }}

空闲时的线程解决

咱们为了不影响性能,独自为空闲的连贯检测开一个线程。

在初始化的创立:

/** * 初始化闲暇时测验 * @since 1.5.0 */private void initTestOnIdle() {    if(StringUtil.isNotEmpty(validQuery)) {        ScheduledExecutorService idleExecutor = Executors.newSingleThreadScheduledExecutor();        idleExecutor.scheduleAtFixedRate(new Runnable() {            @Override            public void run() {                testOnIdleCheck();            }        }, super.testOnIdleIntervalSeconds, testOnIdleIntervalSeconds, TimeUnit.SECONDS);        LOG.debug("Test on idle config with interval seonds: " + testOnIdleIntervalSeconds);    }}

testOnIdleCheck 实现如下:

/** * 验证所有的闲暇连贯是否无效 * @since 1.5.0 */private void testOnIdleCheck() {    LOG.debug("start check test on idle");    for(IPooledConnection pc : this.pool) {        if(!pc.isBusy()) {            checkValid(pc);        }    }    LOG.debug("finish check test on idle");}

开源地址

所有源码均已开源:

jdbc-pool

应用形式和常见的连接池一样。

maven 引入

<dependency>    <groupId>com.github.houbb</groupId>    <artifactId>jdbc-pool</artifactId>    <version>1.5.0</version></dependency>

测试代码

PooledDataSource source = new PooledDataSource();source.setDriverClass("com.mysql.jdbc.Driver");source.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8");source.setUser("root");source.setPassword("123456");source.setMinSize(1);// 初始化source.init();Connection connection = source.getConnection();System.out.println(connection.getCatalog());Connection connection2 = source.getConnection();System.out.println(connection2.getCatalog());

日志

[DEBUG] [2020-07-18 10:50:54.536] [main] [c.g.h.t.p.d.PooledDataSource.getFreeConnection] - 从连接池中获取连贯test[DEBUG] [2020-07-18 10:50:54.537] [main] [c.g.h.t.p.d.PooledDataSource.getConnection] - 开始扩容连接池大小,step: 1[DEBUG] [2020-07-18 10:50:54.548] [main] [c.g.h.t.p.d.PooledDataSource.getConnection] - 从扩容后的连接池中获取连贯test

小结

到这里,一个简略版本的连接池就曾经实现了。

常见的连接池,比方 dbcp/c3p0/druid/jboss-pool/tomcat-pool 其实都是相似的。

万变不离其宗,实现只是一种思维的差异化示意而已。

然而有哪些有余呢?

性能方面,咱们为了简略,都是间接应用 synchronized 保障并发平安,这样性能会绝对于乐观锁,或者是无锁差一些。

自定义方面,比方 druid 能够反对用户自定义拦截器,增加注入避免 sql 注入,耗时统计等等。

页面治理,druid 比拟优异的一点就是自带页面治理,这一点对于日常保护也比拟敌对。