MyBatis 数据源(DataSource)深度解析:从原理到实践(Unpooled/Pooled 与第三方连接池)
MyBatis 的数据源(DataSource)是连接数据库的核心组件,负责管理数据库连接的创建、复用与销毁,直接影响系统的性能与稳定性。系统梳理 MyBatis 内置数据源的实现原理、连接池机制,以及生产环境中主流第三方连接池(HikariCP、Druid)的整合方案,帮助你理解 “连接管理” 的本质并选择合适的数据源。
数据源核心概念与接口规范
在深入 MyBatis 实现前,需先明确数据源的通用规范 —— 所有数据源都遵循 JDBC 标准接口 javax.sql.DataSource,该接口定义了获取数据库连接的核心方法:
1 2 3 4 5 6 7 8 9 10 11 12
| 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 的工厂接口 |
UnpooledDataSourceFactory、PooledDataSourceFactory |
MyBatis 内置数据源:UnpooledDataSource(无池实现)
UnpooledDataSource 是 MyBatis 最简单的数据源实现,每次获取连接都会创建新的 Connection 对象,不进行连接复用,适合简单测试场景,不推荐生产环境使用。
核心原理:每次请求创建新连接
(1)驱动初始化与注册
UnpooledDataSource 在类加载时,会将 DriverManager 中已注册的 JDBC 驱动缓存到 registeredDrivers(避免重复注册),并在首次获取连接时初始化驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class UnpooledDataSource implements DataSource { private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();
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 { if (!registeredDrivers.containsKey(driver)) { try { Class<?> driverType; if (driverClassLoader != null) { driverType = Class.forName(driver, true, driverClassLoader); } else { driverType = Resources.classForName(driver); } 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:
1 2 3 4 5 6 7 8 9 10
| 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(),核心逻辑是 “初始化驱动 → 创建新连接 → 配置连接属性”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| private Connection doGetConnection(Properties properties) throws SQLException { initializeDriver(); Connection connection = DriverManager.getConnection(url, properties); configureConnection(connection); return connection; }
private void configureConnection(Connection conn) throws SQLException { 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:连接状态管理器
PoolState 是 PooledDataSource 的内部类,通过两个集合维护连接状态,并记录连接池的统计信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class PoolState { protected final List<PooledConnection> idleConnections = new ArrayList<>(); protected final List<PooledConnection> activeConnections = new ArrayList<>();
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() 方法—— 将连接归还到池,而非真正关闭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| public class PooledConnection implements InvocationHandler { private final PooledDataSource dataSource; private final Connection realConnection; private final Connection proxyConnection; private long checkoutTimestamp; private long createdTimestamp; private long lastUsedTimestamp; private int connectionTypeCode; 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 ); }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if ("close".equals(methodName)) { dataSource.pushConnection(this); return null; } 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(),核心逻辑是 “优先从空闲池取连接 → 无空闲连接则创建新连接 → 连接池满则等待”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| private PooledConnection popConnection(String username, String password) throws SQLException { long t = System.currentTimeMillis(); long waitTime = 0;
while (true) { synchronized (state) { if (!state.idleConnections.isEmpty()) { PooledConnection conn = state.idleConnections.remove(0); conn.setLastUsedTimestamp(System.currentTimeMillis()); state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; return conn; }
if (state.activeConnections.size() < poolMaximumActiveConnections) { Connection realConn = dataSource.getConnection(username, password); PooledConnection conn = new PooledConnection(realConn, this); state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; return conn; }
state.hadToWaitCount++; waitTime += System.currentTimeMillis() - t; t = System.currentTimeMillis(); try { state.wait(poolTimeToWait); } catch (InterruptedException e) { break; }
if (waitTime >= poolTimeToWait) { state.claimedOverdueConnectionCount++; throw new SQLException("PooledDataSource timeout waiting for connection."); } } } }
|
(2)归还连接:pushConnection ()
当调用代理连接的 close() 时,会触发 pushConnection(),核心逻辑是 “检查连接有效性 → 空闲池未满则归还到空闲池 → 空闲池满则关闭连接”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| protected void pushConnection(PooledConnection conn) throws SQLException { synchronized (state) { state.activeConnections.remove(conn);
if (conn.isValid()) { if (state.idleConnections.size() < poolMaximumIdleConnections) { conn.setLastUsedTimestamp(System.currentTimeMillis()); state.idleConnections.add(conn); state.accumulatedCheckoutTime += System.currentTimeMillis() - conn.getCheckoutTimestamp(); } else { conn.invalidate(); state.badConnectionCount++; closeRealConnection(conn.getRealConnection()); } } else { state.badConnectionCount++; closeRealConnection(conn.getRealConnection()); }
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)
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>
|
(2)配置数据源(MyBatis 全局配置文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <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"/> <property name="maximumPoolSize" value="10"/> <property name="minimumIdle" value="2"/> <property name="idleTimeout" value="300000"/> <property name="connectionTimeout" value="30000"/> </dataSource> </environment> </environments> </configuration>
|
2. Druid:阿里开源连接池(功能最全)
Druid 是阿里巴巴开源的连接池,支持监控、防 SQL 注入、动态配置等高级功能,适合对监控和安全要求高的生产环境,整合步骤如下:
(1)引入依赖(Maven)
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>
|
(2)配置数据源(MyBatis 全局配置文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <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"/> <property name="maxActive" value="10"/> <property name="minIdle" value="2"/> <property name="maxWait" value="30000"/> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <property name="validationQuery" value="SELECT 1"/> <property name="filters" value="stat,wall"/> </dataSource> </environment> </environments> </configuration>
|
(3)启用 Druid 监控(可选)
Druid 提供 Web 监控界面,可通过 Spring 配置启用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <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. 数据源选择优先级(生产环境)
- HikariCP:首选,性能最优,轻量无依赖,适合大多数场景;
- Druid:次选,功能丰富(监控、防注入),适合对监控和安全要求高的场景;
- PooledDataSource:仅用于中小型项目或 Demo,不推荐高并发生产环境;
- UnpooledDataSource:仅用于测试,禁止生产环境使用。
2. 性能优化关键参数(通用)
| 参数类型 |
优化建议 |
理由 |
| 最大活跃连接数(maxActive/maximumPoolSize) |
10~20(根据数据库最大连接数调整) |
过多连接会导致数据库线程切换开销增大,过少会导致等待 |
| 最小空闲连接数(minIdle) |
2~5(略高于平均空闲连接数) |
避免频繁创建 / 关闭连接,平衡资源占用与响应速度 |
| 空闲连接超时时间(idleTimeout) |
300~600 秒(5~10 分钟) |
避免空闲连接长期占用资源,同时减少连接重建频率 |
| 连接超时时间(connectionTimeout) |
10~30 秒 |
避免线程长期阻塞,快速失败以触发重试机制 |
3. 常见问题与解决方案
(1)连接泄露(连接未归还)
(2)连接失效(数据库重启后连接不可用)
- 原因:连接池中的空闲连接长时间未使用,被数据库主动关闭;
- 解决方案:
- 开启连接心跳检测(如 Druid 的
validationQuery、HikariCP 的 connectionTestQuery);
- 调整
idleTimeout 小于数据库的连接超时时间(如 MySQL 的 wait_timeout 默认 8 小时,可设 idleTimeout=3600000 即 1 小时)。
(3)高并发下连接池满(等待超时)
- 原因:最大活跃连接数不足,或业务逻辑执行时间过长;
- 解决方案:
- 临时调大
maxActive(需确认数据库承载能力);
- 优化慢 SQL(如添加索引、减少关联查询),缩短连接占用时间;
- 引入异步处理,减少同步请求对连接的占用。
总结
MyBatis 数据源是连接数据库的 “桥梁”,其实现从简单的无池(UnpooledDataSource)到内置连接池(PooledDataSource),再到第三方成熟连接池(HikariCP/Druid),覆盖了不同场景的需求:
- 学习与测试:使用
UnpooledDataSource 或 PooledDataSource 理解原理;
- 生产环境:优先选择 HikariCP(性能)或 Druid(功能),并合理配置连接池参数;
- 性能优化:核心是 “控制连接数量、复用连接、避免泄露、检测失效连接”