MyBatis 内置连接池原理详解!

开发
这篇文章,我们将深入分析 MyBatis 内置的连接池源码,包括设计原理、类结构,以及核心方法的实现等。

MyBatis 是一个流行的持久层框架,提供了一个简单且灵活的方式来访问数据库,它内置了一个连接池来管理数据库连接。这篇文章,我们将深入分析 MyBatis 内置的连接池源码,包括设计原理、类结构,以及核心方法的实现等。

一、连接池原理

MyBatis 内置的连接池采用了传统的 Java JDBC 连接方式,它负责管理数据库连接的创建、维护和销毁。连接池的设计可以避免每次请求数据库时都重新创建连接,从而提高性能。

  • 连接的创建与管理:MyBatis 使用PooledDataSource类来创建和管理数据库连接。该类实现了DataSource接口,并使用标准的 JDBC API 来获取连接。它会维护一个连接的池,每当请求新的连接时,首先会检查连接池中是否有可用的连接,如果有,则直接返回;如果没有,则创建新的连接。
  • 连接的复用:通过连接池,MyBatis可以重用已经创建的连接。当请求完成后,该连接不会被关闭,而是返回到连接池中,以便后续请求再次使用。这种方式显著减少了创建连接的开销。
  • 连接的关闭与回收:在应用程序的生命周期中,连接池还需要定期检查并关闭超时未使用的连接,以维护资源的有效性。在 MyBatis中,可通过设置最大连接数、最大空闲时间等参数来控制。

二、核心源码分析

Mybatis的源码类整体结构如下图(本文基于 MyBatis 3.5.7):

下面,我们具体分析几个核心的类:

1. PooledDataSource

在 MyBatis 中,PooledDataSource 是其内置连接池的实现,负责管理可重用数据库连接。其核心概念是通过维护一组连接的池,减少频繁创建和销毁连接所带来的性能开销。

PooledDataSource 类在 MyBatis 的 org.apache.ibatis.datasource 包中实现,与多个其他类一起协同工作来管理连接。其结构如下:

  • PooledDataSource:主要的连接池类,管理连接的创建、分配和回收。
  • PooledConnection:对每个 JDBC 连接的包装类,封装了 JDBC Connection 对象,管理连接的状态。

2.初始化连接池

在构造函数中,PooledDataSource 提供了多种方式来初始化连接池,包括一些基本参数,如数据库 URL、用户名和密码,连接池的大小(默认为10)以及最大闲置时间(默认为30秒),这些参数可以通过 MyBatis 配置文件设置。

3.获取连接

在 PooledDataSource 类中,定义了两个关键的方法来获取数据库连接:

  @Override
public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
    return popConnection(username, password).getProxyConnection();
}

这两个方法内部都会调用popConnection方法,源码如下:

  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;

    while (conn == null) {
        synchronized (state) {
            if (!state.idleConnections.isEmpty()) {
                // Pool has available connection
                conn = state.idleConnections.remove(0);
                if (log.isDebugEnabled()) {
                    log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                }
            } else {
                // Pool does not have available connection
                if (state.activeConnections.size() < poolMaximumActiveConnections) {
                    // Can create new connection
                    conn = new PooledConnection(dataSource.getConnection(), this);
                    if (log.isDebugEnabled()) {
                        log.debug("Created connection " + conn.getRealHashCode() + ".");
                    }
                } else {
                    // Cannot create new connection
                    PooledConnection oldestActiveConnection = state.activeConnections.get(0);
                    long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                    if (longestCheckoutTime > poolMaximumCheckoutTime) {
                        // Can claim overdue connection
                        state.claimedOverdueConnectionCount++;
                        state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                        state.accumulatedCheckoutTime += longestCheckoutTime;
                        state.activeConnections.remove(oldestActiveConnection);
                        if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                            try {
                                oldestActiveConnection.getRealConnection().rollback();
                            } catch (SQLException e) {
                                log.debug("Bad connection. Could not roll back");
                            }
                        }
                        conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                        conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
                        conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
                        oldestActiveConnection.invalidate();
                        if (log.isDebugEnabled()) {
                            log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
                        }
                    } else {
                        // Must wait
                        try {
                            if (!countedWait) {
                                state.hadToWaitCount++;
                                countedWait = true;
                            }
                            if (log.isDebugEnabled()) {
                                log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                            }
                            long wt = System.currentTimeMillis();
                            state.wait(poolTimeToWait);
                            state.accumulatedWaitTime += System.currentTimeMillis() - wt;
                        } catch (InterruptedException e) {
                            break;
                        }
                    }
                }
            }
            if (conn != null) {
                // ping to server and check the connection is valid or not
                if (conn.isValid()) {
                    if (!conn.getRealConnection().getAutoCommit()) {
                        conn.getRealConnection().rollback();
                    }
                    conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                    state.activeConnections.add(conn);
                    state.requestCount++;
                    state.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    }
                    state.badConnectionCount++;
                    localBadConnectionCount++;
                    conn = null;
                    if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
                        if (log.isDebugEnabled()) {
                            log.debug("PooledDataSource: Could not get a good connection to the database.");
                        }
                        throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }
                }
            }
        }

    }
    if (conn == null) {
        if (log.isDebugEnabled()) {
            log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
        }
        throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }
    return conn;
}

popConnection方法会检查当前池中是否有可用连接:

  • 如果连接池中有空闲连接,就从 idleConnections 列表中移除并返回该连接。这里使用 remove(0) 方法获取并移除列表的第一个元素,确保获取的是最旧的连接。
  • 如果没有空闲连接,且当前活跃连接数小于最大活跃连接数,可以创建新的连接。
  • 如果已有的活跃连接已达到最大数量,且没有空闲连接,方法会检查最旧的活跃连接的检出时间。
  • 如果超时,则将其声明为过期连接,并尝试获取其持有的资源;否则,当前线程必须等待可用连接。
  • 如果必须等待连接,方法会调用 wait(),使当前线程挂起,同时记录等待时间和次数。
  • 在成功获得连接后,还需检查其有效性。如果有效,则进行一些准备工作,如回滚事务、设置连接属性等。
  • 若连接无效,则增加坏连接计数,并尝试重新获取连接。
  • 如果无法得到有效连接,抛出 SQLException 表示连接池发生了严重错误。
  • 如果一切正常则返回连接

总结:

  • popConnection方法负责从 MyBatis 的连接池中获取连接,详细设计考虑了多个方面,包括:
  • 连接的复用与生命周期管理:通过维护空闲连接和活跃连接,有效控制连接的生命周期,减少开销。
  • 并发控制:使用 synchronized 确保在多线程环境下的安全性,避免连接状态混乱。
  • 连接的有效性检查:在获取连接后,确保连接仍是有效的,避免因无效连接导致的错误。
  • 超时管理与连接等待:通过定义一定的等待策略,避免因连接争用引发的资源竞争。

4.关闭连接

关闭连接池的核心源码如下:

当需要关闭连接池时,forceCloseAll 方法会被调用,它会遍历连接池中的所有PooledConnection实例并调用它们的 forceClose 方法,确保所有连接被关闭并释放资源。

三、PooledConnection

PooledConnection 类是包装 JDBC Connection 对象的类,提供了额外的功能和方法。其核心代码如下:

public PooledConnection(Connection connection, PooledDataSource dataSource) {
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true;
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}

PooledConnection 维护着一个真实的 JDBC 连接实例,以及一个可用状态标志。可以通过 isAvailable 来判断连接是否可以使用。

四、连接池的管理逻辑

在 PooledDataSource 的实现中,连接的获取和归还之间包含了若干管理逻辑,以确保连接的有效性和可用性:

  • 检查连接可用性:在连接使用之前,会检查连接状态,确保其未被占用。
  • 连接超时管理:将连接的最大闲置时间作为管理参数,如果连接在一定时间内未使用,则通过定时任务回收这些连接。
  • 连接数限制:池中最大连接数的限制可以防止过多的连接被创建,避免因资源浪费而降低性能。

五、优缺点

优点:

  • 简单易用:MyBatis 自带连接池实现简单,适合快速开发和少量数据库操作的场景。
  • 无外部依赖:作为 MyBatis 的一部分,使用内置连接池不需要额外添加依赖。

缺点:

  • 功能单一:与其他成熟的连接池相比,MyBatis 自带连接池的功能较为简单,缺乏一些高级特性,如连接健康检查、连接存活时间管理等。
  • 性能限制:在高并发环境下,MyBatis 内置连接池可能遭遇性能瓶颈,易出现连接争用或等待。

六、总结

这篇文章,我们详细地分析了MyBatis内置线程池的原理以及核心源码分析,内置的连接池适合于简单的应用场景,随着项目复杂度的增加,特别是在高并发的情况下,使用如 HikariCP、C3P0 等更成熟的连接池实现。

责任编辑:赵宁宁 来源: 猿java
相关推荐

2015-04-27 09:50:45

Java Hibern连接池详解

2019-12-30 15:30:13

连接池请求PHP

2019-11-27 10:31:51

数据库连接池内存

2011-06-01 13:54:10

MySQL

2018-02-07 16:23:58

连接池内存池AI

2011-05-19 09:53:33

数据库连接池

2009-09-22 14:52:55

Hibernate p

2009-09-22 16:04:50

Hibernate连接

2022-11-11 09:41:04

连接池微服务数据库

2009-06-17 16:22:45

Hibernate连接

2009-06-15 13:46:00

netbeans设置数据库连接池

2009-07-15 11:00:48

proxool连接池

2009-06-17 09:59:46

Hibernate 连

2010-06-25 10:36:27

Java连接池

2020-02-03 15:15:27

Druid连接池性能超出竞品

2009-06-24 07:53:47

Hibernate数据

2009-12-25 15:38:12

ADO连接池

2022-07-19 13:51:47

数据库Hikari连接池

2024-07-15 08:20:24

2021-03-24 09:06:01

MySQL长连接短连接
点赞
收藏

51CTO技术栈公众号