Mybatis深入之数据库连接池原理

Mybatis深入之数据库连接池原理

简介

主要记录Mybatis数据库连接池实现原理、如何使用连接池来管理数据库连接的、连接池如何向外提供数据库连接、当外部调用使用完成之后是如何将数据库连接放回数据库连接池的。

准备

有前面的相关文章的铺垫、这里就不再从Mybatis数据库相关信息的初始化以及何时创建一个真正的数据库连接并且向外提供使用的。这两方面的过程可以参见Mybatis深入之DataSource实例化过程Mybatis深入之获取数据库连接两篇文章。

了解Mybatis数据库连接池如何配置

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driverClassName}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>  
  • 标签<dataSource type="POOLED">指明使用Mybatis自带数据库连接池
  • 标签<transactionManager type="JDBC"/>指明使用JDBC形式管理事务、参见Mybatis深入之事务管理

原理分析

当Mybatis初始化完成之后、根据上面的配置以及前面的文章知道、 环境中的DataSource实例是PooledDataSource、环境中的Transaction实例是JdbcTransaction。

Mybatis深入之获取数据库连接中知道、当执行第一个sql语句时才会尝试获取数据库连接详细的可以看前面的文章、这里直接上获取数据库连接的关键代码:

  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }
  • 获取PooledConnection——popConnection(dataSource.getUsername(), dataSource.getPassword())
  • 调用PooledConnection的getProxyConnection()方法返回真正连接的代理连接

当尝试获取数据库连接的时候会使用数据库连接池功能、详细获取数据库连接的代码Mybatis深入之获取数据库连接有简单介绍。

在想要了解数据库连接池原理之前需要了解一个Mybatis用来存放数据库连接池状态的类:PoolState、PooledConnection以及PooledDataSource类属性以及之间的关系:

  • PoolState数据库连接池状态类、其内部有标识数据库连接池状态的各个属性、重点是两个属性:List<PooledConnection> idleConnections用于存放空闲状态的数据库连接。List<PooledConnection> activeConnections用于存放活动状态的连接、也就是正在使用的数据库连接。
  • PoolState内部持有一个DataSource引用、在PoolState被实例化时初始化、主要用于展示当前数据库连接的一些配置信息、比如用户名、密码等。
  • PooledConnection内部持有一个PooledDataSource、同样在PooledConnection被构造时实例化PooledDataSource、其中有两个属性private long createdTimestamp;

    private long lastUsedTimestamp;用来标识当前PooledConnection的创建时间和最后使用时间、用来判断此连接是否过期。

  • PooledDataSource 简单的线程安全的数据库连接池、对外提供数据库连接池功能。
  • -

获取PooledConnection

从上面可以知道程序中使用了数据库连接池之后、获取的数据库连接是从PooledDataSource中方法popConnection(String username, String password)获取的、获取代码:

  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
    //恒成立、直到上面定义的PooledConnection被正确实例化或者程序异常中止
    while (conn == null) {
      //synchronized (state)是保证PooledDataSource是一个线程安全的数据库连接池的原因。
      synchronized (state) {
        //如果当前数据库连接池中有空闲状态的数据库连接、则直接取出一个作为当前方法执行结果返回。
        if (state.idleConnections.size() > 0) {
          // 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
            //创建一个内部持有真正数据库连接的PooledConnection
            conn = new PooledConnection(dataSource.getConnection(), this);
            @SuppressWarnings("unused")
            //used in logging, if enabled
            //真正的数据库连接
            Connection realConn = conn.getRealConnection();
            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()) {
                oldestActiveConnection.getRealConnection().rollback();
              }
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              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) {
          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 + 3)) {
              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. 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第2步。
  2. 查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第三步;
  3. 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第4步。
  4. 线程等待,循环2步

下面是从网络上摘抄的一个流程图:

获取最终数据库连接

通过PooledConnection.getProxyConnection()来获取最终数据库连接。

  public Connection getProxyConnection() {
    return proxyConnection;
  }

这里只要弄清楚proxyConnection是什么就行,在上面一节获取PooledDataSource时候的popConnection方法中知道PooledConnection是在这个方法里面创建的、调用的都是相同的PooledConnection构造方法:

  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);
  }
  • 重点关注最后一行代码this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  • 这里是动态代理模式来使用当前类PooledConnection来代理Connection类。
  • 关于动态代理设计模式相关的这里不是重点、可以看一下Java设计模式之代理

所以最终返回的是真正的Connection的代理类PooledConnection。至于为什么要这样做、接着看。

数据库连接使用完毕放回连接池

不使用数据库连接池时、正常使用数据库连接的情况下、当使用完毕之后我们就会调用其close()方法来关闭连接、避免资源浪费。但是当使用了数据库连接池之后、一个数据库连接被使用完之后就不再是调用其close方法关闭掉、而是应该将这个数据库连接放回连接池、那么我们就要拦截Connection.close()方法、将这个Connection放回连接池、而不是关闭。

使用动态代理的方式实现上述功能、PooledConnection实现了InvocationHandler接口、并提供了invoke方法的实现。当调用Connection的方法时会执行PooledConnection的invoke方法:

/*
 * Required for InvocationHandler implementation.
 *  * @param proxy  - not used
 * @param method - the method to be executed
 * @param args   - the parameters to be passed to the method
 * @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
   */
  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);
      }
    }
  }
  • 拦截Connection执行方法、
  • 如果是close方法、放回数据库连接池
  • 如果不是、放掉、由Connection继续执行
  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包含真正Connection重新创建一个PooledConnection并放到连接池中
          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++;
      }
    }
  }
  1. 如果当前需要关闭的数据库连接以失效、废弃不管PoolState的badConnectionCount自增1
  2. 如果当前连接有效并且当前数据库连接池中空闲连接数没有达到数据库连接池连接最大数、并且此数据库连接是所期望放回数据库连接池的。重新根据当前PooledConnection中的真正数据库连接Connection创建一个新的PooledConnection并放回数据库连接池中。
  3. 如果当前空闲连接数已达到数据库连接池容量最大值、或者不是所期望的数据库连接、关闭连接

到这里、数据库连接池原理就结束了。

补充

Mybatis的数据库连接池简单、线程安全。当与spring结合的时候通常也可以使用第三方数据库连接池、将有关数据库连接、事务都交由spring去管理。

更多内容:Mybatis 目录

时间: 2024-10-10 16:35:05

Mybatis深入之数据库连接池原理的相关文章

数据库连接池原理

——连接池用什么数据结构实现? —— 实现连接池的代码 —— 线程安全问题 [数据库连接池的设计思路及java实现][ http://blog.csdn.net/shijinupc/article/details/7836129] [Java的JDBC数据库连接池实现方法][ http://developer.51cto.com/art/200907/137300.htm ]   ?   [设计数据库连接池,要考虑哪些问题?] 1. 有一个简单的函数从连接池中得到一个 Connection. 2

DBCP数据库连接池原理分析

在比较大的项目中,需要不断的从数据库中获取数据,Java中则使用JDBC连接数据库,但是获取数据库的连接可是相当耗时的操作,每次连接数据库都获得 .销毁数据库连接,将是很大的一个开销.为了解决这种开销,则使用了对象池的技术.程序启动时,先创建一定数量的数据库连接对象,然后只要一用到就直接从对象池中取出连接对象,然后使用完成后不对其销毁,而是再返回到对象池中,这样使连接池对象能反复使用,虽然增加了启动时所需要的时间但是提高了响应速度,对象池也相当于是一个缓存. 这里有DBCP的一些配置参数,通过这

数据库连接池原理详解与自定义连接池实现

实现原理 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数制约.无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量.连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中. 连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使

spark 大型项目实战(七):用户访问session分析(七) --数据库连接池原理

**文章地址:http://www.haha174.top/article/details/257789** 1.谈谈数据库连接池的原理 ------------- 这次我们采取技术演进的方式来谈谈数据库连接池的技术出现过程及其原理,以及当下最流行的开源数据库连接池jar包. 一.早期我们怎么进行数据库操作 1.原理:一般来说,java应用程序访问数据库的过程是: ①装载数据库驱动程序: ②通过jdbc建立数据库连接: ③访问数据库,执行sql语句: ④断开数据库连接. 2.代码 Public

数据库连接池原理及应用

1.运作原理 在实际应用开发中,特别是在WEB应用系统中,如果JSP.Servlet或EJB使用JDBC直接访问数据库中的数据,每一次数据访问请求都必须经历建立数据库连接.打开数据库.存取数据和关闭数据库连接等步骤,而连接并打开数据库是一件既消耗资源又费时的工作,如果频繁发生这种数据库操作,系统的性能必然会急剧下降,甚至会导致系统崩溃.数据库连接池技术是解决这个问题最常用的方法,在许多应用程序服务器(例如:Weblogic,WebSphere,JBoss)中,基本都提供了这项技术,无需自己编程,

【笔记】python的sqlalchemy数据库连接池原理的说明

sqlalchemy数据库连接池的使用方式是延迟初始化,就是说一开始你调用create_engine(...)后创建的那个数据库池是空的,你后面通过session.connection()或者engine.connect()才开始创建连接, 每当你创建一个连接,你调用engine.pool.status()就可以看到数据库连接池处于什么状态,下面说明以下status()的输出说明: 'Pool size: 16  Connections in pool: 1 Current Overflow:

总结代理模式 并根据数据库连接池原理来模拟实现自己的数据库连接池工具类

代理模式 需求:对系统中已有的某个类的功能,进行扩展(增强) 要求:在不修改源码的情况下,对已有的功能进行增强 静态代理 抽象接口:保证包含增强功能的对象和原有功能的对象,对外提供相同的方法 目标对象:封装了原有功能 代理对象:封装了增强功能和原有功能(通过持有一个目标对象的引用实现) 问题:代码不够灵活,产生大量冗余工作 动态代理 动态代理的实现API: Proxy : 帮助程员产生代理对象,提供产生代理类和代理对象的静态方法 InvocationHandler : 句柄接口,拦截到,所有代理

数据库连接池两种配置方式详解

数据库连接池:   负责分配.管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而再不是重新建立一个:释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏:数据库连接池原理:   连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用.而连接的建立.断开都由

【Java EE 学习第16天】【dbcp数据库连接池】【c3p0数据库连接池】

零.回顾之前使用的动态代理的方式实现的数据库连接池: 代码: 1 package day16.utils; 2 3 import java.io.IOException; 4 import java.lang.reflect.InvocationHandler; 5 import java.lang.reflect.Method; 6 import java.lang.reflect.Proxy; 7 import java.sql.Connection; 8 import java.sql.D