0%

mybatis之DataSource

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 的工厂接口 UnpooledDataSourceFactoryPooledDataSourceFactory

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 {
// 缓存已注册的 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

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 {
// 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 的内部类,通过两个集合维护连接状态,并记录连接池的统计信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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() 方法—— 将连接归还到池,而非真正关闭:

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; // 原生 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(),核心逻辑是 “优先从空闲池取连接 → 无空闲连接则创建新连接 → 连接池满则等待”:

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) {
// 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(),核心逻辑是 “检查连接有效性 → 空闲池未满则归还到空闲池 → 空闲池满则关闭连接”:

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) {
// 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)
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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 全局配置文件)
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"/>
<!-- 配置 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)
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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 全局配置文件)
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"/>
<!-- 配置 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 配置启用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 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:

      1
      2
      3
      4
      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(功能),并合理配置连接池参数;
  • 性能优化:核心是 “控制连接数量、复用连接、避免泄露、检测失效连接”

欢迎关注我的其它发布渠道