对于ORM框架而言,数据源的组织是一个非常重要的一部分,这直接影响到框架的性能问题。本文将通过对MyBatis框架的数据源结构进行详尽的分析,并且深入解析MyBatis的连接池。
本文首先会讲述MyBatis的数据源的分类,然后会介绍数据源是如何加载和使用的。紧接着将分类介绍UNPOOLED、POOLED和JNDI类型的数据源组织;期间我们会重点讲解POOLED类型的数据源和其实现的连接池原理。
本文结构如下:
- 一、MyBatis数据源DataSource分类
- 二、数据源DataSource的创建过程
- 三、 DataSource什么时候创建Connection对象
- 四、不使用连接池的UnpooledDataSource
- 五、为什么要使用连接池?
- 六、使用了连接池的PooledDataSource
一、Mybatis数据源分类
mybatis数据源实现类在mybatis的dataSource包中:
Mybatis将数据源分为三种:
JNDI 数据源 : 使用JNDI方式数据源
POOLED数据眼: 使用连接池数据源
UNPOOLED 数据源 : 不使用连接池数据源
即:
相应的Mybatis内部分别以实现 javax.sql.DataSource 接口的 PooledDataSource和 UnPooledDataSource 实现 POOLED和UNPOOLED数据源。(关于数据源创建细节请看下面章节)
JNDI数据源则通过 javax.naming.Context 上下文生成数据源。
二、数据源DataSource创建过程
数据源配置如下:
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/pmdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
Mybatis数据源的创建过程:
Mybatis初始化阶段 就会创建好数据源,具体创建数据源的时机发生在解析mybatis XML配置文件<environments></environments> 节点下的<dataSource></dataSouorce>节点:
//context :dataSource节点数据
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
//获取dataSource配置的类型 (POOLED、UNPOOLED、JNDI)
String type = context.getStringAttribute("type");
//将dataSource下的username、password等信息解析为Properties
Properties props = context.getChildrenAsProperties();
//根据dataSource的type类型(别名机制)获取到对应的DateSource实现类,并实例化该类
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
创建DataSource最关键的一步在: DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
打开该方法resolveClass方法实现,看它到底做了什么:
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (TYPE_ALIASES.containsKey(key)) {
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
resolveClass方法核心功能就是根据XML dataSource节点配置的type属性找到对应的实现类:
如上图所示:
根据配置的type别名找到Factory,然后创建出对应的DataSource
- JNDI : JndiDataSourceFactory
- POOLED: PooledDataSourceFactory
- UNPOOLED: UnpooledDataSourceFactory
Mybatis创建DataSource之后会将其放在Configuration的Environment中,供以后使用。
三、DataSource什么时候创建Connection对象
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = (User) sqlSession.selectOne("selectByPrimaryKey", 1);
如上图所示,前三行代码都不会去创建javax.sql.Connection,当执行到 selectOne("selectByPrimaryKey", 1);时,才会去真正创建Connection对象:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
flushStatements();
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
那对于UNPOOLED类型DataSource的实现UnpooledDataSource是怎么样实现getConnection方法呢?请看一下节。
四、不使用连接池的UnpooledDataSource
//UnpoolDataSource 创建Connection对象
private Connection doGetConnection(Properties properties) throws SQLException {
//1. 初始化驱动
initializeDriver();
//2. 创建Connection对象
Connection connection = DriverManager.getConnection(url, properties);
//3. 配置Connection
configureConnection(connection);
return connection;
}
如上代码所示,流程如下:
- 1. 初始化驱动 : 判断驱动是否加载到内存中,若有则直接取出,否则创建驱动
- 2. 创建Connecion : 调用DriverManager创建相应的Connection对象
- 3. 配置Connection: 配置Connection对象一些默认配置项
- 4. 返回DataSource对象: 返回创建好的Connection对象供以使用
总结:从上述的代码中可以看到,我们每调用一次getConnection()方法,都会通过DriverManager.getConnection()返回新的java.sql.Connection实例。
五、为什么要使用连接池?
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
long start = System.currentTimeMillis();
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mall", "root", "123456");
System.out.println("创建Connection对象耗时 : " + String.valueOf(System.currentTimeMillis() - start));
String sql = "select * from mmall_user where id = 1";
start = System.currentTimeMillis();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
System.out.println("查询语句耗时:" + String.valueOf(System.currentTimeMillis() - start));
while(resultSet.next()){
System.out.println("id = " + resultSet.getInt(1) + " username = " + resultSet.getString(2));
}
resultSet.close();
statement.close();
connection.close();
}
创建Connection对象耗时 789ms, 而查询语句才耗时7ms.(不排除数据库数据少的原因,但是查询耗时一般不会超过789ms)
一次查询请求创建Connection对象耗时789ms。要知道100ms对于Java来说都是很奢侈的。(一个Connection对象耗时 700ms,10000 * 700 = 116分钟,10000次请求只创建对象就耗时116分钟,这是根本不能接受的)
所以使用连接池是非常有必要的。
六、使用了连接池的PooledDataSource
了解连接池之前,先了解两个参数概念:
idleConnections : 空闲Connection对象,当其他请求需要创建Connection时,直接到ideaConnection取出一个连接,可以减少资源、耗时。
activeConnections : 活动Connection对象,记录当前正在被请求所使用的Connection对象,当一次请求使用完一个Connection时,不将其立即销毁,而是放到idleConnection缓存池里面。
对于UnpooledDataSource每次请求都会创建一个新的Connection对象,当请求结束后会执行Connection.cloes()方法关闭该Connection.
PooledDataSource是如何创建的Connection的呢?
@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();
}
数据源type设置为 POOLED,当实例化DataSource时会根据别名实例化出 PooledDataSource对象。
当调用getConnection方法创建Connection时,最终会调用 popConnecion方法并返回一个代理对象。
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) {
//判断连接池中是否还有空闲Connection对象,若有则直接返回一个Connection
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 {//没有空闲Connection对象
//当前活动对象个数是小于最大活动数量 则会生成一个新的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 {//判断老的活动对象是否超过poolMaximumCheckoutTime时间
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
//超过poolMaximumCheckoutTime时间,则尝试结束该Connection对象线程,并返回重用Connection
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) {
/*
Just log a message for debug and continue to execute the following
statement like nothing happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
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 {//没有超时,则等待该Connection线程结束
// 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;
}
综上所述,大致流程如下:
- 1. 若IdleConnection中是否有空闲的连接对象,则直接返回Connection
- 2. 判断当前activeConnection数量是否小于poolMaximumActiveConnections(活动连接最大数量),若小于,则和Unpooled方式一样创建新的Connection对象并返回。若大于,则会判断当前所有活动连接的占用时间是否超时,若超时则停止该Connection,并直接返回供其他请求使用。若没有超时,则等待Connection使用完毕后再返回。
连接池对Connection.close()的处理:
PooledDataSource中除了popConnection方法,还有一个pushConnection方法
pushConnection方法会将使用完毕的Connection放入idleConnections缓存池中,供其他请求继续使用。
传统的jdbc连接使用完Connection之后,会手动执行Connection.cloes()方法关闭连接。
Pooled连接池为了重复利用Connection减少不必要的开销,对Connection.cloes做了动态代理。
也就是说,在Pooled模式下,若我们手动执行connecion.cloes(),实际上并不会执行原生Connection.close方法。而是通过PooledConnection对原生Connection做动态代理,把close方法映射到 pushConnection方法上:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//若执行close方法,实际上会代理执行pushConnection方法
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
以上就是本文 《深入理解Mybatis原理》 02-Mybatis数据源与连接池 的全部内容,
上述内容如有不妥之处,还请读者指出,共同探讨,共同进步!
@author : jackcheng1117@163.com