mybatis之DataSource

MyBatis 数据源(DataSource)深度解析:从原理到实践(Unpooled/Pooled 与第三方连接池)

MyBatis 的数据源(DataSource)是连接数据库的核心组件,负责管理数据库连接的创建、复用与销毁,直接影响系统的性能与稳定性。系统梳理 MyBatis 内置数据源的实现原理、连接池机制,以及生产环境中主流第三方连接池(HikariCP、Druid)的整合方案,帮助你理解 “连接管理” 的本质并选择合适的数据源。

数据源核心概念与接口规范

在深入 MyBatis 实现前,需先明确数据源的通用规范 —— 所有数据源都遵循 JDBC 标准接口 javax.sql.DataSource,该接口定义了获取数据库连接的核心方法:

public interface DataSource {
    // 获取数据库连接
    Connection getConnection() throws SQLException;
    // 带用户名/密码的连接获取
    Connection getConnection(String username, String password) throws SQLException;
    // 获取连接池配置(可选)
    PrintWriter getLogWriter() throws SQLException;
    void setLogWriter(PrintWriter out) throws SQLException;
    // 连接超时时间
    int getLoginTimeout() throws SQLException;
    void setLoginTimeout(int seconds) throws SQLException;
}

MyBatis 对数据源的封装遵循 “工厂模式”:通过 DataSourceFactory 接口创建不同类型的 DataSource,核心实现如下:

组件 作用 MyBatis 内置实现
DataSource 管理数据库连接 UnpooledDataSource(无池)、PooledDataSource(有池)
DataSourceFactory 创建 DataSource 的工厂接口 UnpooledDataSourceFactoryPooledDataSourceFactory

MyBatis 内置数据源:UnpooledDataSource(无池实现)

UnpooledDataSource 是 MyBatis 最简单的数据源实现,每次获取连接都会创建新的 Connection 对象,不进行连接复用,适合简单测试场景,不推荐生产环境使用。

核心原理:每次请求创建新连接

(1)驱动初始化与注册

UnpooledDataSource 在类加载时,会将 DriverManager 中已注册的 JDBC 驱动缓存到 registeredDrivers(避免重复注册),并在首次获取连接时初始化驱动:

public class UnpooledDataSource implements DataSource {
    // 缓存已注册的 JDBC 驱动(key:驱动类名,value:驱动实例)
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();

    // 静态代码块:加载 DriverManager 中已注册的驱动
    static {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            registeredDrivers.put(driver.getClass().getName(), driver);
        }
    }

    // 初始化驱动(首次获取连接时调用)
    private synchronized void initializeDriver() throws SQLException {
        // 若驱动未注册,则加载并注册到 DriverManager
        if (!registeredDrivers.containsKey(driver)) {
            try {
                Class<?> driverType;
                // 加载驱动类(支持自定义类加载器)
                if (driverClassLoader != null) {
                    driverType = Class.forName(driver, true, driverClassLoader);
                } else {
                    driverType = Resources.classForName(driver);
                }
                // 创建驱动实例,通过代理类注册到 DriverManager
                Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();
                DriverManager.registerDriver(new DriverProxy(driverInstance));
                registeredDrivers.put(driver, driverInstance);
            } catch (Exception e) {
                throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
            }
        }
    }
}

关键知识点:JDBC 驱动的自动注册
以 MySQL 驱动(com.mysql.cj.jdbc.Driver)为例,其源码中包含静态代码块,在 Class.forName() 加载类时会自动注册到 DriverManager

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // 类加载时自动注册驱动
            DriverManager.registerDriver(new Driver());
        } catch (SQLException e) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

这就是为什么早期 JDBC 代码中 Class.forName("com.mysql.cj.jdbc.Driver") 后,无需手动调用 DriverManager.registerDriver() 的原因。

(2)获取连接的核心流程

UnpooledDataSource 的所有 getConnection() 重载方法最终都会调用 doGetConnection(),核心逻辑是 “初始化驱动 → 创建新连接 → 配置连接属性”:

private Connection doGetConnection(Properties properties) throws SQLException {
    // 1. 初始化 JDBC 驱动(确保驱动已注册)
    initializeDriver();
    // 2. 通过 DriverManager 创建新的数据库连接(每次都是新连接)
    Connection connection = DriverManager.getConnection(url, properties);
    // 3. 配置连接属性(自动提交、事务隔离级别)
    configureConnection(connection);
    return connection;
}

// 配置连接的自动提交和隔离级别
private void configureConnection(Connection conn) throws SQLException {
    // 设置自动提交(默认 false,MyBatis 手动管理事务)
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
        conn.setAutoCommit(autoCommit);
    }
    // 设置事务隔离级别(若配置了则覆盖默认值)
    if (defaultTransactionIsolationLevel != null) {
        conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
}

优缺点与适用场景

优点 缺点 适用场景
实现简单,无额外依赖 每次创建新连接,耗时较长(连接创建是 IO 密集型操作) 本地测试、小流量非生产环境
无连接池维护开销 无法控制连接数量,高并发下可能导致数据库连接耗尽 学习 MyBatis 数据源原理的 Demo

MyBatis 内置数据源:PooledDataSource(连接池实现)

PooledDataSource 是 MyBatis 内置的连接池实现,通过复用连接解决 UnpooledDataSource 的性能问题,核心是维护 “空闲连接池” 和 “活跃连接池”,实现连接的高效管理。

连接池核心设计:PoolState 与 PooledConnection

PooledDataSource 的核心是 PoolState 组件和 PooledConnection 包装类,分别负责 “连接状态管理” 和 “连接代理”。

(1)PoolState:连接状态管理器

PoolStatePooledDataSource 的内部类,通过两个集合维护连接状态,并记录连接池的统计信息:

public class PoolState {
    // 1. 空闲连接池:存放可用的连接(已创建但未被使用)
    protected final List<PooledConnection> idleConnections = new ArrayList<>();
    // 2. 活跃连接池:存放正在被使用的连接
    protected final List<PooledConnection> activeConnections = new ArrayList<>();

    // 3. 连接池统计信息(用于监控和调优)
    protected long requestCount = 0L;          // 总请求连接次数
    protected long accumulatedRequestTime = 0L;// 总请求等待时间
    protected long accumulatedCheckoutTime = 0L;// 总连接占用时间
    protected long claimedOverdueConnectionCount = 0L; // 超时连接数
    protected long badConnectionCount = 0L;     // 无效连接数
    // ... 其他统计字段
}
(2)PooledConnection:连接代理包装类

PooledConnection 实现 InvocationHandler 接口,对原生 Connection 进行代理,核心是重写 close() 方法—— 将连接归还到池,而非真正关闭:

public class PooledConnection implements InvocationHandler {
    private final PooledDataSource dataSource; // 关联的连接池
    private final Connection realConnection;   // 原生 JDBC 连接
    private final Connection proxyConnection;   // 代理连接(对外暴露)
    private long checkoutTimestamp;             // 连接取出时间戳
    private long createdTimestamp;              // 连接创建时间戳
    private long lastUsedTimestamp;             // 最后使用时间戳
    private int connectionTypeCode;             // 连接类型编码(区分不同用户/URL)
    private boolean valid;                      // 连接是否有效

    // 构造方法:创建代理连接
    public PooledConnection(Connection connection, PooledDataSource dataSource) {
        this.realConnection = connection;
        this.dataSource = dataSource;
        this.proxyConnection = (Connection) Proxy.newProxyInstance(
            Connection.class.getClassLoader(),
            new Class[]{Connection.class},
            this
        );
    }

    // 代理逻辑:重写 close() 方法,归还连接到池
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        // 1. 若调用 close() 方法,将连接归还到连接池(而非关闭)
        if ("close".equals(methodName)) {
            dataSource.pushConnection(this); // 归还连接到空闲池
            return null;
        }
        // 2. 其他方法(如 executeQuery())直接调用原生连接
        try {
            if (!Object.class.equals(method.getDeclaringClass())) {
                checkConnection(); // 检查连接是否有效
            }
            return method.invoke(realConnection, args);
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

    // 检查连接是否有效(避免使用已失效的连接)
    private void checkConnection() throws SQLException {
        if (!valid) {
            throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
        }
    }
}

连接池核心流程:获取与归还连接

PooledDataSource 的核心逻辑集中在 popConnection()(获取连接)和 pushConnection()(归还连接)两个方法。

(1)获取连接:popConnection ()

当调用 getConnection() 时,会触发 popConnection(),核心逻辑是 “优先从空闲池取连接 → 无空闲连接则创建新连接 → 连接池满则等待”:

private PooledConnection popConnection(String username, String password) throws SQLException {
    long t = System.currentTimeMillis();
    long waitTime = 0;

    while (true) {
        synchronized (state) {
            // 1. 检查空闲池是否有可用连接
            if (!state.idleConnections.isEmpty()) {
                // 从空闲池头部取出连接(FIFO 策略)
                PooledConnection conn = state.idleConnections.remove(0);
                conn.setLastUsedTimestamp(System.currentTimeMillis());
                state.activeConnections.add(conn);
                state.requestCount++;
                state.accumulatedRequestTime += System.currentTimeMillis() - t;
                return conn; // 返回可用连接
            }

            // 2. 空闲池无连接,检查是否可创建新连接(未达最大连接数)
            if (state.activeConnections.size() < poolMaximumActiveConnections) {
                // 创建新的原生连接
                Connection realConn = dataSource.getConnection(username, password);
                // 包装为 PooledConnection 并添加到活跃池
                PooledConnection conn = new PooledConnection(realConn, this);
                state.activeConnections.add(conn);
                state.requestCount++;
                state.accumulatedRequestTime += System.currentTimeMillis() - t;
                return conn;
            }

            // 3. 连接池已满(活跃连接数达上限),需等待
            state.hadToWaitCount++;
            waitTime += System.currentTimeMillis() - t;
            t = System.currentTimeMillis();
            try {
                // 等待指定时间(poolTimeToWait),超时则抛出异常
                state.wait(poolTimeToWait);
            } catch (InterruptedException e) {
                break;
            }

            // 4. 等待超时,检查是否有过期连接(超过 poolMaximumCheckoutTime 未归还)
            if (waitTime >= poolTimeToWait) {
                state.claimedOverdueConnectionCount++;
                throw new SQLException("PooledDataSource timeout waiting for connection.");
            }
        }
    }
}
(2)归还连接:pushConnection ()

当调用代理连接的 close() 时,会触发 pushConnection(),核心逻辑是 “检查连接有效性 → 空闲池未满则归还到空闲池 → 空闲池满则关闭连接”:

protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized (state) {
        // 1. 从活跃池移除连接
        state.activeConnections.remove(conn);

        // 2. 检查连接是否有效(避免归还无效连接)
        if (conn.isValid()) {
            // 2.1 空闲池未满,归还到空闲池尾部
            if (state.idleConnections.size() < poolMaximumIdleConnections) {
                conn.setLastUsedTimestamp(System.currentTimeMillis());
                state.idleConnections.add(conn);
                state.accumulatedCheckoutTime += System.currentTimeMillis() - conn.getCheckoutTimestamp();
            } else {
                // 2.2 空闲池已满,关闭连接(避免维护过多空闲连接)
                conn.invalidate();
                state.badConnectionCount++;
                closeRealConnection(conn.getRealConnection());
            }
        } else {
            // 3. 连接无效,直接关闭并记录统计
            state.badConnectionCount++;
            closeRealConnection(conn.getRealConnection());
        }

        // 4. 唤醒等待连接的线程
        state.notifyAll();
    }
}

连接池核心配置参数

PooledDataSource 提供多个可配置参数,用于调整连接池性能,常见配置如下(在 mybatis-config.xml 中设置):

参数名 作用 默认值 建议值(生产)
poolMaximumActiveConnections 最大活跃连接数(同时使用的连接上限) 10 10~20(根据数据库并发能力调整)
poolMaximumIdleConnections 最大空闲连接数(空闲时保留的连接上限) 5 3~5(避免过多空闲连接占用资源)
poolMaximumCheckoutTime 连接最大占用时间(超时未归还则视为过期) 20000ms 30000ms(30 秒)
poolTimeToWait 获取连接的最大等待时间(超时抛异常) 20000ms 10000ms(10 秒)
poolPingEnabled 是否开启连接心跳检测(避免连接失效) false true(生产环境建议开启)

优缺点与适用场景

优点 缺点 适用场景
连接复用,减少连接创建开销,性能优于无池实现 实现简单,功能不如第三方连接池(如无监控、无动态扩容) 中小型项目、对连接池功能要求不高的场景
控制连接数量,避免数据库连接耗尽 高并发下性能可能瓶颈(相比 HikariCP/Druid) 学习连接池原理的 Demo 项目

生产环境首选:第三方连接池整合

MyBatis 内置连接池(PooledDataSource)功能简单,生产环境中推荐使用第三方成熟连接池(如 HikariCP、Druid),它们在性能、稳定性、监控能力上更优。

1. HikariCP:Spring Boot 默认连接池(性能最优)

HikariCP 是目前性能最好的 Java 连接池,以 “轻量、快速、低延迟” 著称,是 Spring Boot 2.x 的默认连接池,整合 MyBatis 步骤如下:

(1)引入依赖(Maven)
<!-- HikariCP 依赖 -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.0.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
(2)配置数据源(MyBatis 全局配置文件)
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <!-- 配置 HikariCP 数据源 -->
            <dataSource type="com.zaxxer.hikari.HikariDataSource">
                <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
                <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
                <!-- HikariCP 核心配置 -->
                <property name="maximumPoolSize" value="10"/> <!-- 最大活跃连接数 -->
                <property name="minimumIdle" value="2"/>      <!-- 最小空闲连接数 -->
                <property name="idleTimeout" value="300000"/>  <!-- 空闲连接超时时间(5分钟) -->
                <property name="connectionTimeout" value="30000"/> <!-- 连接超时时间(30秒) -->
            </dataSource>
        </environment>
    </environments>
</configuration>

2. Druid:阿里开源连接池(功能最全)

Druid 是阿里巴巴开源的连接池,支持监控、防 SQL 注入、动态配置等高级功能,适合对监控和安全要求高的生产环境,整合步骤如下:

(1)引入依赖(Maven)
<!-- Druid 依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.20</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
(2)配置数据源(MyBatis 全局配置文件)
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <!-- 配置 Druid 数据源 -->
            <dataSource type="com.alibaba.druid.pool.DruidDataSource">
                <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
                <!-- Druid 核心配置 -->
                <property name="maxActive" value="10"/>          <!-- 最大活跃连接数 -->
                <property name="minIdle" value="2"/>            <!-- 最小空闲连接数 -->
                <property name="maxWait" value="30000"/>        <!-- 连接超时时间(30秒) -->
                <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 连接检测间隔(1分钟) -->
                <property name="validationQuery" value="SELECT 1"/> <!-- 心跳检测 SQL -->
                <!-- 开启 Druid 监控(可选) -->
                <property name="filters" value="stat,wall"/>     <!-- stat:监控,wall:防 SQL 注入 -->
            </dataSource>
        </environment>
    </environments>
</configuration>
(3)启用 Druid 监控(可选)

Druid 提供 Web 监控界面,可通过 Spring 配置启用:

<!-- Spring 配置文件 -->
<bean id="statViewServlet" class="com.alibaba.druid.support.http.StatViewServlet">
    <init-param>
        <param-name>loginUsername</param-name>
        <param-value>druid</param-value> <!-- 监控页面用户名 -->
    </init-param>
    <init-param>
        <param-name>loginPassword</param-name>
        <param-value>druid123</param-value> <!-- 监控页面密码 -->
    </init-param>
</bean>
<servlet-mapping>
    <servlet-name>statViewServlet</servlet-name>
    <url-pattern>/druid/*</url-pattern> <!-- 监控页面访问路径 -->
</servlet-mapping>

访问 http://localhost:8080/druid 即可进入监控界面,查看连接池状态、SQL 执行情况等。

数据源选择与性能优化建议

1. 数据源选择优先级(生产环境)

  1. HikariCP:首选,性能最优,轻量无依赖,适合大多数场景;
  2. Druid:次选,功能丰富(监控、防注入),适合对监控和安全要求高的场景;
  3. PooledDataSource:仅用于中小型项目或 Demo,不推荐高并发生产环境;
  4. UnpooledDataSource:仅用于测试,禁止生产环境使用。

2. 性能优化关键参数(通用)

参数类型 优化建议 理由
最大活跃连接数(maxActive/maximumPoolSize) 10~20(根据数据库最大连接数调整) 过多连接会导致数据库线程切换开销增大,过少会导致等待
最小空闲连接数(minIdle) 2~5(略高于平均空闲连接数) 避免频繁创建 / 关闭连接,平衡资源占用与响应速度
空闲连接超时时间(idleTimeout) 300~600 秒(5~10 分钟) 避免空闲连接长期占用资源,同时减少连接重建频率
连接超时时间(connectionTimeout) 10~30 秒 避免线程长期阻塞,快速失败以触发重试机制

3. 常见问题与解决方案

(1)连接泄露(连接未归还)
  • 原因:未正确关闭 SqlSessionConnection,导致连接长期占用;

  • 解决方案:

    1. 使用try-with-resources自动关闭SqlSession:

      try (SqlSession session = sqlSessionFactory.openSession()) {
          UserMapper mapper = session.getMapper(UserMapper.class);
          // 业务逻辑
      }
    2. 开启连接池超时检测(如 HikariCP 的 leakDetectionThreshold),定位泄露代码。

(2)连接失效(数据库重启后连接不可用)
  • 原因:连接池中的空闲连接长时间未使用,被数据库主动关闭;
  • 解决方案:
    1. 开启连接心跳检测(如 Druid 的 validationQuery、HikariCP 的 connectionTestQuery);
    2. 调整 idleTimeout 小于数据库的连接超时时间(如 MySQL 的 wait_timeout 默认 8 小时,可设 idleTimeout=3600000 即 1 小时)。
(3)高并发下连接池满(等待超时)
  • 原因:最大活跃连接数不足,或业务逻辑执行时间过长;
  • 解决方案:
    1. 临时调大 maxActive(需确认数据库承载能力);
    2. 优化慢 SQL(如添加索引、减少关联查询),缩短连接占用时间;
    3. 引入异步处理,减少同步请求对连接的占用。

总结

MyBatis 数据源是连接数据库的 “桥梁”,其实现从简单的无池(UnpooledDataSource)到内置连接池(PooledDataSource),再到第三方成熟连接池(HikariCP/Druid),覆盖了不同场景的需求:

  • 学习与测试:使用 UnpooledDataSourcePooledDataSource 理解原理;
  • 生产环境:优先选择 HikariCP(性能)或 Druid(功能),并合理配置连接池参数;
  • 性能优化:核心是 “控制连接数量、复用连接、避免泄露、检测失效连接”