【Mybatis源码分析】03-DataSourceFactory与DataSource

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

Mybatis中DataSourceFactory是负责创建DataSource对象的,Mybatis一共为我们提供了3种DataSourceFactory的实现,不同的DataSourceFactory创建不同的DataSource,分别为UnpooledDataSourceFactory,PooledDataSourceFactory和JndiDataSourceFactory。具体使用哪种DataSourceFactory我们在《SqlSessionFactory的创建过程》中分析过了,下面详细说一下PooledDataSourceFactory和UnpooledDataSourceFactory。PooledDataSourceFactory继承了UnpooledDataSourceFactory,除了构造函数中创建的DataSource不同其余完全相同。

    public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
      public PooledDataSourceFactory() {
        this.dataSource = new PooledDataSource();
      }
    }
    public UnpooledDataSourceFactory() {
      this.dataSource = new UnpooledDataSource();
    }

UnpooledDataSourceFactory比较简单除了getDataSource方法以外还有另一个public的方法setProperties。

    public void setProperties(Properties properties) {
      Properties driverProperties = new Properties();
      MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
      for (Object key : properties.keySet()) {
        String propertyName = (String) key;
        if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
          String value = properties.getProperty(propertyName);
          driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
        } else if (metaDataSource.hasSetter(propertyName)) {
          String value = (String) properties.get(propertyName);
          Object convertedValue = convertValue(metaDataSource, propertyName, value);
          metaDataSource.setValue(propertyName, convertedValue);
        } else {
          throw new DataSourceException("Unknown DataSource property: " + propertyName);
        }
      }
      if (driverProperties.size() > 0) {
        metaDataSource.setValue("driverProperties", driverProperties);
      }
    }

这个方法就将datasource标签的子元素设置到构造方法创建的DataSource对象中去,其中包括url,username,password等必要的属性,如果属性中存在DataSource中没有与之对应的Getter方法则抛异常。简单配置如下:

    <dataSource type="POOLED">
          <property name="driver" value="${driver}"/>
          <property name="url" value="${url}"/>
          <property name="username" value="${username}"/>
          <property name="password" value="${password}"/>
    </dataSource>

而getDataSource方法则是原封不动的放回DataSource对象。

    public DataSource getDataSource() {
      return dataSource;
    }

下面分别看看这两种DataSource,UnpooledDataSource和PooledDataSource。

  1. UnpooledDataSource 中getConnection方法直接调用了doGetConnection方法,方法中可以看到每次获取Connection对象都是通过DriverManager的getConnection方法创建一个新的Connection对象,所以说UnpooledDataSource就像它的名字一样非池化的数据源不会缓存创建的数据库连接,所以对于频繁操作数据库的应用来说不建议使用这种DataSource。
    private Connection doGetConnection(Properties properties) throws SQLException {
      initializeDriver();
      Connection connection = DriverManager.getConnection(url, properties);
      configureConnection(connection);
      return connection;
    }
  1. PooledDataSource 中有2个比较重要的成员变量,一个是state一个是dataSource。dataSource就是UnpooledDataSource,PooledDataSource产生的数据库连接就是通过dataSource产生的所以可以说PooledDataSource是一个静态代理。state是一个PoolState对象它代表了当前连接池的状态详情,之所以叫池化的数据源就是因为state有两个List,分别代表活跃的数据库连接和空闲的数据库连接。
    public class PoolState {
    
      protected PooledDataSource dataSource;
    
      protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
      protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();

我们分析一下PooledDataSource的getConnection方法。

    public Connection getConnection() throws SQLException {
      return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
    }
    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) {
                    /*
                       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 {
                // 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.当前活跃连接数小于我们设置的最大活跃连接数poolMaximumActiveConnections(默认10),new一个PooledConnection,其中包含一个从dataSource中获取的Connection和当前连接池对象并且实现了InvocationHandler接口,所以PooledConnection是Connection的动态代理,我们稍后先纤细分析这个代理版的Connection。

2.当前活跃连接数大于等于poolMaximumActiveConnections时又分为两种情况:

2.1 活跃列表中第一个连接(也就是时间最长的)在列表中时长超过poolMaximumCheckoutTime(默认20秒),则将其从列表中移除,取出真正的Connection对象放在一个新的PooledConnection中,将旧的PooledConnection设置为无效,这样一来当调用PooledConnection的非close方法(当然也除了从Object继承的方法)回去检测当前PooledConnection是否无效,如果无效则抛出异常。

2.2活跃列表中第一个连接在列表中时长小于poolMaximumCheckoutTime,则会等待poolTimeToWait(默认20秒),直到超时或者有连接被释放,重新竞争新一轮连接。

PooledConnection是如何做到连接重新利用的?

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      String methodName = method.getName();
      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);
        }
      }
    }

先说一下else的情况,当调用Connection的非close方法会检测一下当前代理连接是否无效(上面分析过超过poolMaximumCheckoutTime时就是设置为无效),如果无效则抛出异常。

到这我有个疑问,如果线程一顺利执行完checkConnection方法,线程二在从连接池获取连接的时候碰到了没有空闲连接并且活跃列表中第一个连接超过了poolMaximumCheckoutTime而执行了

    oldestActiveConnection.getRealConnection().rollback();

然后线程一执行了目标对象的commit方法,岂不是造成了逻辑错乱,不知道这是不是Mybatis的一个Bug。

然后说一下释放连接的过程,当调用close方法的时候会调用连接池的pushConnection方法。

    protected void pushConnection(PooledConnection conn) throws SQLException {
    
      synchronized (state) {
        state.activeConnections.remove(conn);
        if (conn.isValid()) {
          if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
            state.accumulatedCheckoutTime += conn.getCheckoutTime();
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
            state.idleConnections.add(newConn);
            newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
            newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
            conn.invalidate();
            if (log.isDebugEnabled()) {
              log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
            }
            state.notifyAll();
          } else {
            state.accumulatedCheckoutTime += conn.getCheckoutTime();
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            conn.getRealConnection().close();
            if (log.isDebugEnabled()) {
              log.debug("Closed connection " + conn.getRealHashCode() + ".");
            }
            conn.invalidate();
          }
        } else {
          if (log.isDebugEnabled()) {
            log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
          }
          state.badConnectionCount++;
        }
      }
    }

大致逻辑就是首先将代理连接从活跃列表中移除

当前空闲连接小于最大空闲连接,则复用真实连接重新包装一个代理连接放入空闲列表中。

否则就调用真实连接的close方法,以保证不能超过最大空闲连接数。

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值