MyBatis 内置连接池原理详解!

你好,我是猿java

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

1. 连接池原理

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

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

2. 核心源码分析

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

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

2.1 PooledDataSource

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

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

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

2.1.1 初始化连接池

img

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

2.1.2 获取连接

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

1
2
3
4
5
6
7
8
9
  @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方法,源码如下:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
  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 确保在多线程环境下的安全性,避免连接状态混乱。
  • 连接的有效性检查:在获取连接后,确保连接仍是有效的,避免因无效连接导致的错误。
  • 超时管理与连接等待:通过定义一定的等待策略,避免因连接争用引发的资源竞争。

2.1.3 关闭连接

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

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

3. PooledConnection

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

1
2
3
4
5
6
7
8
9
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 来判断连接是否可以使用。

4. 连接池的管理逻辑

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

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

5. 优缺点

5.1 优点

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

5.2 缺点

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

6. 总结

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

7. 学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing