# 数据源分类
先看一下之前 xml 配置写的内容:
<?xml version="1.0" encoding="UTF-8" ?> | |
<!DOCTYPE configuration | |
PUBLIC "-//mybatis.org//DTD Config 3.0//EN" | |
"http://mybatis.org/dtd/mybatis-3-config.dtd"> | |
<configuration> | |
<environments default="development"> | |
<environment id="development"> | |
<transactionManager type="JDBC"/> | |
<dataSource type="POOLED"> | |
</dataSource> | |
</environment> | |
</environments> | |
</configuration> |
看 dataSource
标签,Mybatis 将数据源分为三类:
- UNPOOLED:不使用连接池
- POOLED:使用连接池
- JNDI:使用 JNDI 实现的数据源
这里不讲 JNDI,只讨论前两种。
先看一下相关的类图:
看一下最顶上的接口:
public interface DataSource extends CommonDataSource, Wrapper { | |
Connection getConnection() throws SQLException; | |
Connection getConnection(String username, String password) | |
throws SQLException; | |
} |
之前使用 JDBC(导入 jar 包那会),是通过 DriverManager
方法获取连接的
Connection conn = DriverManager.getConnection(url,user,password); |
很显然,Mybatis 是通过 DataSource 的实现类获取连接的(其实最终还是使用了 DriverManager
),连接的创建和关闭时非常消耗系统资源的,通过 DriverManager
获取的连接,每次操作都请求依次物理连接,使用完后关闭连接,频繁的建立,关闭连接会消耗资源。
创建一个连接往往比执行一个 sql 语句还要费时间。
一般比较常用的 DataSource
实现,都是采用池化技术,就是在一开始就创建好 N 个连接,这样之后使用就无需再次进行连接,而是直接使用现成的 Connection
对象进行数据库操作。
POOLED
就是使用池化技术, UNPOOLED
就是非池化技术。
# 连接池
看上图,池化技术中,PooledDataSource 依赖于 UnpooledDataSource,这点待会在源码讲解中也会明显看出来。
# UNPOOLED
每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。
该数据源只需要配置 6 种属性,这在源码中表现如下:
public class UnpooledDataSource implements DataSource { | |
private ClassLoader driverClassLoader; | |
private Properties driverProperties; | |
private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap(); | |
private String driver; | |
private String url; | |
private String username; | |
private String password; | |
private Boolean autoCommit; // 这个不算在 6 个配置属性里面 | |
private Integer defaultTransactionIsolationLevel; // 默认事务隔离级别 | |
private Integer defaultNetworkTimeout; // 超时连接的默认等待时间 | |
// ... | |
} |
实现 DataSource
,需要知道,连接最终都需要用户名和密码,所以这个接口的两个方法在 UmpooledDataSource
都调用最终的一个方法 doGetConnection(String, String)
:
public Connection getConnection() throws SQLException { | |
return this.doGetConnection(this.username, this.password); | |
} | |
public Connection getConnection(String username, String password) throws SQLException { | |
return this.doGetConnection(username, password); | |
} | |
// 最终走向的方法 | |
private Connection doGetConnection(String username, String password) throws SQLException { | |
Properties props = new Properties(); | |
if (this.driverProperties != null) { | |
props.putAll(this.driverProperties); | |
} | |
if (username != null) { | |
props.setProperty("user", username); | |
} | |
if (password != null) { | |
props.setProperty("password", password); | |
} | |
return this.doGetConnection(props); | |
} |
它将数据库的连接信息也给添加到 Properties
对象中进行存放,并交给下一个 doGetConnection
来处理,继续向下走:
private Connection doGetConnection(Properties properties) throws SQLException { | |
// 若未初始化驱动,需要先初始化,内部维护了一个 Map 来记录初始化信息 | |
this.initializeDriver(); | |
// 传统的获取连接的方式 | |
Connection connection = DriverManager.getConnection(this.url, properties); | |
// 对连接进行额外的一些配置 | |
this.configureConnection(connection); | |
return connection; | |
} |
可以看到其实最终还是使用了 DriverManager
获取连接,这就是非池化技术,也是获取一个连接对象的流程。在池化技术种表现不同的就是,连接不会马上被销毁,而是被维护在链表中从而实现复用。
# POOLED
该数据源除了非池化配置的属性,还有很多配置属性,这里就不一一列举了。因为维护了一个连接池(其实就是一个集合,说得高大上),所以需要考虑并发问题,考虑如何合理存放大量连接对象,合理分配等。在原始创建连接时,还是依赖的非池化技术:
private final UnpooledDataSource dataSource; // 属性 |
先看一下实现 DataSource
接口:
public Connection getConnection() throws SQLException { | |
return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection(); | |
} | |
public Connection getConnection(String username, String password) throws SQLException { | |
return this.popConnection(username, password).getProxyConnection(); | |
} |
看到 pop
, Proxy
,可以猜测整个连接池有点像栈集合,获取对象进行了一个代理。
popConnection
方法很长,为了内容完整性,我们还是一起来看一下:
private PooledConnection popConnection(String username, String password) throws SQLException { | |
boolean countedWait = false; | |
// 返回的是 PooledConnection 对象, | |
PooledConnection conn = null; | |
long t = System.currentTimeMillis(); | |
int localBadConnectionCount = 0; | |
while(conn == null) { | |
synchronized(this.state) { // 加锁,因为有可能很多个线程都需要获取连接对象 | |
PoolState var10000; | |
//PoolState 存了两个 List,一个是空闲列表,一个是活跃列表 | |
if (!this.state.idleConnections.isEmpty()) { // 有空闲连接时,可以直接分配 Connection | |
conn = (PooledConnection)this.state.idleConnections.remove(0); //ArrayList 中取第一个元素 | |
if (log.isDebugEnabled()) { | |
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); | |
} | |
// 如果已经没有多余的连接可以分配,那么就检查一下活跃连接数是否达到最大的分配上限,如果没有,就 new 一个 | |
} else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) { | |
// 注意 new 了之后并没有立即往 List 里面塞,只是存了一些基本信息 | |
// 我们发现,这里依靠 UnpooledDataSource 创建了一个 Connection 对象,并将其封装到 PooledConnection 中 | |
conn = new PooledConnection(this.dataSource.getConnection(), this); | |
if (log.isDebugEnabled()) { | |
log.debug("Created connection " + conn.getRealHashCode() + "."); | |
} | |
// 以上条件都不满足,那么只能从之前的连接中寻找了,看看有没有那种卡住的链接 | |
// 由于网络问题有可能之前的连接一直被卡住,然而正常情况下早就结束并且可以使用了,所以这里相当于是优化也算是一种捡漏的方式 | |
} else { | |
// 获取最早创建的连接 | |
PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0); | |
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); | |
// 判断是否超过最大的使用时间 | |
if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) { | |
// 超时统计信息(不重要) | |
++this.state.claimedOverdueConnectionCount; | |
var10000 = this.state; | |
var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; | |
var10000 = this.state; | |
var10000.accumulatedCheckoutTime += longestCheckoutTime; | |
// 从活跃列表中移除此链接信息 | |
this.state.activeConnections.remove(oldestActiveConnection); | |
// 如果开启事务,还需要回滚一下 | |
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { | |
try { | |
oldestActiveConnection.getRealConnection().rollback(); | |
} catch (SQLException var15) { | |
log.debug("Bad connection. Could not roll back"); | |
} | |
} | |
// 这里就根据之前的连接对象直接 new 一个新的连接(注意使用的还是之前的 Connection 对象,只是被重新封装了) | |
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 { | |
// 确实是没得用了,只能卡住了(阻塞) | |
// 然后记录一下有几个线程在等待当前的任务搞完 | |
try { | |
if (!countedWait) { | |
++this.state.hadToWaitCount; | |
countedWait = true; | |
} | |
if (log.isDebugEnabled()) { | |
log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection."); | |
} | |
long wt = System.currentTimeMillis(); | |
this.state.wait((long)this.poolTimeToWait); // 要是超过等待时间还是没等到,只能放弃 | |
// 注意这样的话 con 就为 null 了 | |
var10000 = this.state; | |
var10000.accumulatedWaitTime += System.currentTimeMillis() - wt; | |
} catch (InterruptedException var16) { | |
break; | |
} | |
} | |
} | |
// 经过之前的操作,已经成功分配到连接对象的情况下 | |
if (conn != null) { | |
if (conn.isValid()) { // 是否有效 | |
if (!conn.getRealConnection().getAutoCommit()) { // 清理之前遗留的事务操作 | |
conn.getRealConnection().rollback(); | |
} | |
conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password)); | |
conn.setCheckoutTimestamp(System.currentTimeMillis()); | |
conn.setLastUsedTimestamp(System.currentTimeMillis()); | |
// 添加到活跃表中 | |
this.state.activeConnections.add(conn); | |
// 统计信息(不重要) | |
++this.state.requestCount; | |
var10000 = this.state; | |
var10000.accumulatedRequestTime += System.currentTimeMillis() - t; | |
} else { | |
// 无效的连接,直接抛异常 | |
if (log.isDebugEnabled()) { | |
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); | |
} | |
++this.state.badConnectionCount; | |
++localBadConnectionCount; | |
conn = null; | |
if (localBadConnectionCount > this.poolMaximumIdleConnections + this.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."); | |
} else { | |
return conn; | |
} | |
} |
面试绝对不会问代码细节,所以这个方法我们看完知道个流程就行了,首先要知道,PoolState 维护两个链表,一个是空闲链表,一个是活跃链表。当我们需要一个连接时:
- 先从空闲链表中获取节点
- 如果没有空闲的节点,并且活跃连接数(活跃链表节点数)没有达到上限,就 new 一个
- 否则就只能到活跃链表最前面拿到最先创建的节点,检查这个节点是否超过最大使用时间,如果可以拿到这个节点,回滚一下这个连接之前的事务,重新封装(不是重新创建)一下,如果拿不到,就会等待阻塞这个节点,超出等待时间,就会记录一些日志,然后
conn=null
结束。 - 最后拿到这个
conn
,如果为null
,就抛出异常;不为null
,这个Connection
然后加入到活跃链表中。
关闭这个连接( connection.close()
,在 try 语句中就会自动调用,关闭连接),需要知道 Mybatis 并没有重定义 Connection,** 我们如何知道系统调用了 connetcion.close()
这个方法?**Mybatis 使用的是代理模式,为真正的 Connection
对象创建一个代理对象,代理对象所有的方法都是调用相应的真正 Connection
对象的方法实现。当代理对象执行 close()
方法时,要特殊处理,不调用真正 Connection
对象的 close()
方法,而是将 Connection
对象添加到连接池中。
class PooledConnection implements InvocationHandler { | |
//...... | |
private static final String CLOSE = "close"; | |
// 所创建它的 datasource 引用 | |
private PooledDataSource dataSource; | |
// 真正的 Connection 对象 | |
private Connection realConnection; | |
// 代理自己的代理 Connection | |
private Connection proxyConnection; | |
//...... | |
} |
实际上,我们调用 PooledDataSource
的 getConnection()
方法 ** 返回的就是这个 proxyConnection
对象。** 当我们调用此 proxyConnection 对象上的任何方法时,都会调用 PooledConnection 对象内 invoke()
方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
String methodName = method.getName(); | |
// 当调用关闭的时候,回收此 Connection 到 PooledDataSource 中 | |
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { | |
dataSource.pushConnection(this); | |
return null; | |
} else { | |
try { | |
if (!Object.class.equals(method.getDeclaringClass())) { | |
checkConnection(); | |
} | |
return method.invoke(realConnection, args); | |
} catch (Throwable t) { | |
throw ExceptionUtil.unwrapThrowable(t); | |
} | |
} | |
} |
我只能感慨,反射学的好,那是真 nb。
# 一些问题
- 何时创建 Connection:我们执行 sql 语句是通过
sqlSession
,这时候 Mybatis 才会调用DataSource
对象创建连接。 UnpooledDataSource
会做的事:先判断driver
驱动是否加载到内存中,实例化 Driver 对象,使用 DriverManager.registerDriver () 方法将其注册到内存中,以供后续使用。之后的内容就熟悉了,本质会调用DriverManager.getConnection()
方法获取连接,配置后返回。
# 参考
https://pdai.tech/md/framework/orm-mybatis/mybatis-y-datasource.html
https://www.yuque.com/qingkongxiaguang/spring/rlgcf7#f8ccf35e
部分图片来源:https://blog.csdn.net/luanlouis