mybatis第十一话 - mybaits getConnection连接数据库的源码分析

到本篇文章,整个Mybatis框架的分析应该是都完了,但是也在我心中产生了几个想法,为什么看不到连接数据库的源码,连接数据库的流程是怎么样的?为什么项目重启的第一次连接查询会很慢?
本文主要探索一下mysql数据库的连接过程!

1.入口SimpleExecutor#prepareStatement

  • 前面的代码就不贴了,从这里开始
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
 Statement stmt;
 //获取连接  ###
 Connection connection = getConnection(statementLog);
 stmt = handler.prepare(connection, transaction.getTimeout());
 handler.parameterize(stmt);
 return stmt;
}

//BaseExecutor
protected Connection getConnection(Log statementLog) throws SQLException {
	//这里用的是springboot项目,事务都是由spring管理的
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

2.事务管理器SpringManagedTransaction

public Connection getConnection() throws SQLException {
  //从事务分析的那篇文章 我们得知如果有事务注解的情况下会先连接 这里就不会再建立新的连接了
  if (this.connection == null) {
  	//无事务的情况下 ### 
    openConnection();
  }
  return this.connection;
}

4.工具类DataSourceUtils

  • openConnection -> getConnection -> doGetConnection
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
	//。。。省略部分代码
	logger.debug("Fetching JDBC Connection from DataSource");
	Connection con = fetchConnection(dataSource);
	//。。。省略部分代码
}
  • 这一块正常是到默认的数据池管理的,例如hikari、druid等,这里直接简化源码流程了,直接来到MysqlDataSource
private static Connection fetchConnection(DataSource dataSource) throws SQLException {
	//用的是mysql的连接 这里就直接到MysqlDataSource	
	Connection con = dataSource.getConnection();
	if (con == null) {
		throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
	}
	return con;
}

5.MysqlDataSource

public java.sql.Connection getConnection() throws SQLException {
    return getConnection(this.user, this.password);
}

//getConnection
protected java.sql.Connection getConnection(Properties props) throws SQLException {
    String jdbcUrlToUse = this.explicitUrl ? this.url : getUrl();

    // URL should take precedence over properties
    //初始化一些连接信息 主机 端口 用户名密码 等
    ConnectionUrl connUrl = ConnectionUrl.getConnectionUrlInstance(jdbcUrlToUse, null);
    Properties urlProps = connUrl.getConnectionArgumentsAsProperties();
    urlProps.remove(PropertyKey.HOST.getKeyName());
    urlProps.remove(PropertyKey.PORT.getKeyName());
    urlProps.remove(PropertyKey.DBNAME.getKeyName());
    urlProps.stringPropertyNames().stream().forEach(k -> props.setProperty(k, urlProps.getProperty(k)));
	//找驱动 ###
    return mysqlDriver.connect(jdbcUrlToUse, props);
}

6.数据库驱动 NonRegisteringDriver

@Override
public java.sql.Connection connect(String url, Properties info) throws SQLException {

try {
    if (!ConnectionUrl.acceptsUrl(url)) {
        /*
         * According to JDBC spec:
         * The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
         * JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
         */
        return null;
    }
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
       switch (conStr.getType()) {
           case SINGLE_CONNECTION:
           		//配置中的driver-class-name: com.mysql.cj.jdbc.Driver ###
               return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

           case FAILOVER_CONNECTION:
           case FAILOVER_DNS_SRV_CONNECTION:
               return FailoverConnectionProxy.createProxyInstance(conStr);

           case LOADBALANCE_CONNECTION:
           case LOADBALANCE_DNS_SRV_CONNECTION:
               return LoadBalancedConnectionProxy.createProxyInstance(conStr);

           case REPLICATION_CONNECTION:
           case REPLICATION_DNS_SRV_CONNECTION:
               return ReplicationConnectionProxy.createProxyInstance(conStr);

           default:
               return null;
       }
     //。。。省略后续代码       
 }

7.构造类ConnectionImpl

public ConnectionImpl(HostInfo hostInfo) throws SQLException {

	//。。。省略部分代码
	try {
	   //创建一个新的IO
	   createNewIO(false);
	
	   unSafeQueryInterceptors();
	
	  AbandonedConnectionCleanupThread.trackConnection(this, this.getSession().getNetworkResources());
	} catch (SQLException ex) {
	  cleanup(ex);
	
	  // don't clobber SQL exceptions
	  throw ex;
	}	

	//。。。省略部分代码
}	

@Override
public void createNewIO(boolean isForReconnect) {
	//加锁连接
    synchronized (getConnectionMutex()) {
        // Synchronization Not needed for *new* connections, but definitely for connections going through fail-over, since we might get the new connection
        // up and running *enough* to start sending cached or still-open server-side prepared statements over to the backend before we get a chance to
        // re-prepare them...

        try {
            if (!this.autoReconnect.getValue()) {
                connectOneTryOnly(isForReconnect);
                return;
            }
			//连接 ###
            connectWithRetries(isForReconnect);
        } catch (SQLException ex) {
            throw ExceptionFactory.createException(UnableToConnectException.class, ex.getMessage(), ex);
        }
    }
}

8.Socket管理类NativeSocketConnection

  • connectWithRetries -> NativeSession#connect -> NativeSocketConnection#connect
@Override
public void connect(String hostName, int portNumber, PropertySet propSet, ExceptionInterceptor excInterceptor, Log log, int loginTimeout) {

    try {
        this.port = portNumber;
        this.host = hostName;
        this.propertySet = propSet;
        this.exceptionInterceptor = excInterceptor;
		//创建一个socket工厂 断点可得到是socketFactory的默认实现StandardSocketFactory
        this.socketFactory = createSocketFactory(propSet.getStringProperty(PropertyKey.socketFactory).getStringValue());
        //连接 ###
        this.mysqlSocket = this.socketFactory.connect(this.host, this.port, propSet, loginTimeout);

        //。。。省略部分代码
        
		//mysql的输出流
        this.mysqlInput = new FullReadInputStream(rawInputStream);
        //mysql的写入流 最终与数据库进行IO写入和读取操作的类
        this.mysqlOutput = new BufferedOutputStream(this.mysqlSocket.getOutputStream(), 16384);
    } catch (IOException ioEx) {
        throw ExceptionFactory.createCommunicationsException(propSet, null, new PacketSentTimeHolder() {
        }, null, ioEx, getExceptionInterceptor());
    }
}

到这里,最终与数据库进行IO写入和读取操作的类就是这两个类FullReadInputStream、BufferedOutputStream

9.连接类StandardSocketFactory

  • 断点可以得知createSocketFactory,最终到StandardSocketFactory#connect,找到以下代码
//获取连接的主机数
InetAddress[] possibleAddresses = InetAddress.getAllByName(this.host);
if (possibleAddresses.length == 0) {
    throw new SocketException("No addresses for host");
}
// save last exception to propagate to caller if connection fails
SocketException lastException = null;
//如果多个地址需要循环连接 只要一个能连接上就截止
for (int i = 0; i < possibleAddresses.length; i++) {
    try {
    	//就当前类 return new Socket();
        this.rawSocket = createSocket(pset);
		//设置一些默认的配置
        configureSocket(this.rawSocket, pset);
		//将地址和端口 检查下并封装
        InetSocketAddress sockAddr = new InetSocketAddress(possibleAddresses[i], this.port);
        //如果不使用临时端口,则绑定到本地端口
        if (localSockAddr != null) {
            this.rawSocket.bind(localSockAddr);
        }
		//通过socket 建立连接
        this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout));
		//如果成功就会break掉
        break;
    } catch (SocketException ex) {
        lastException = ex;
        resetLoginTimeCountdown();
        this.rawSocket = null;
    }
}

10.在现在的项目中,基本上会出现第一次查询时比较慢

在现在的项目中,基本上会出现第一次查询时比较慢,那是因为数据库连接时延迟连接的,简单说明一下Druid连接池
DruidDataSource#CreateConnectionThread#run以线程的形式存在的,找到这段代码

//该方法是通过init进来的
if (emptyWait) {
    // 必须存在线程等待,才创建连接
    if (poolingCount >= notEmptyWaitThreadCount //
            && (!(keepAlive && activeCount + poolingCount < minIdle))
            && !isFailContinuous()
    ) {
    	//利用了Condition锁的消费生产队列模式
        empty.await();
    }

    // 防止创建超过maxActive数量的连接
    if (activeCount + poolingCount >= maxActive) {
        empty.await();
        continue;
    }
}

//。。。省略部分代码

//后续才是数据库的真正连接,然后会交由该数据连接池处理
//最终保存在private volatile DruidConnectionHolder[] connections;
boolean result = put(connection);

//在put添加完后会执行唤醒消费者的操作 同时连接数+1
notEmpty.signal();
notEmptySignalCount++;
  • DruidDataSource#getConnectionInternal
if (maxWait > 0) {
	//notEmpty.awaitNanos(estimate);  如果已经是最大的连接数了 等待指定时长获取连接
	holder = pollLast(nanos);
} else {
	//notEmpty.await(); 直接等待 等待创建新的连接或者是否连接后唤醒
    holder = takeLast();
}

以上就是本章的全部内容了。

上一篇:mybatis第十话 - mybaits整个事务流程的源码分析
下一篇:mysql第一话 - mysql基于docker的安装及使用

云想衣裳花想容,春风拂槛露华浓

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值