博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
源码详解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)
阅读量:3959 次
发布时间:2019-05-24

本文共 37947 字,大约阅读时间需要 126 分钟。

简介

DBCP用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他连接池一样,也具有连接数控制、连接有效性检测、连接泄露控制、缓存语句等功能。目前,tomcat自带的连接池就是DBCP,Spring开发组也推荐使用DBCP,阿里的druid也是参照DBCP开发出来的。

DBCP除了我们熟知的使用方式外,还支持通过JNDI获取数据源,并支持获取JTAXA事务中用于2PC(两阶段提交)的连接对象,本文也将以例子说明。

本文将包含以下内容(因为篇幅较长,可根据需要选择阅读):

  1. DBCP的使用方法(入门案例说明);
  2. DBCP的配置参数详解;
  3. DBCP主要源码分析;
  4. DBCP其他特性的使用方法,如JNDIJTA支持。

使用例子

需求

使用DBCP连接池获取连接对象,对用户数据进行简单的增删改查。

工程环境

JDK:1.8.0_201

maven:3.6.1

IDE:eclipse 4.12

mysql-connector-java:8.0.15

mysql:5.7.28

DBCP:2.6.0

主要步骤

  1. 编写dbcp.properties,设置数据库连接参数和连接池基本参数等。

  2. 通过BasicDataSourceFactory加载dbcp.properties,并获得BasicDataDource对象。

  3. 通过BasicDataDource对象获取Connection对象。

  4. 使用Connection对象对用户表进行增删改查。

创建项目

项目类型Maven Project,打包方式war(其实jar也可以,之所以使用war是为了测试JNDI)。

引入依赖

junit
junit
4.12
test
org.apache.commons
commons-dbcp2
2.6.0
log4j
log4j
1.2.17
mysql
mysql-connector-java
8.0.15

编写dbcp.prperties

路径resources目录下,因为是入门例子,这里仅给出数据库连接参数和连接池基本参数,后面源码会对配置参数进行详细说明。另外,数据库sql脚本也在该目录下。

#连接基本属性driverClassName=com.mysql.cj.jdbc.Driverurl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=trueusername=rootpassword=root#-------------连接池大小和连接超时参数--------------------------------#初始化连接数量:连接池启动时创建的初始化连接数量#默认为0initialSize=0#最大活动连接数量:连接池在同一时间能够分配的最大活动连接的数量, 如果设置为负数则表示不限制#默认为8maxTotal=8#最大空闲连接:连接池中容许保持空闲状态的最大连接数量,超过的空闲连接将被释放,如果设置为负数表示不限制#默认为8maxIdle=8#最小空闲连接:连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接,如果设置为0则不创建#注意:timeBetweenEvictionRunsMillis为正数时,这个参数才能生效。#默认为0minIdle=0#最大等待时间#当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计数),超过时间则抛出异常,如果设置为<=0表示无限等待#默认-1maxWaitMillis=-1

获取连接池和获取连接

项目中编写了JDBCUtils来初始化连接池、获取连接和释放资源等,具体参见项目源码。

路径:cn.zzs.dbcp

// 导入配置文件    Properties properties = new Properties();    InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("dbcp.properties");    properties.load(in);    // 根据配置文件内容获得数据源对象    DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);    // 获得连接    Connection conn = dataSource.getConnection();

编写测试类

这里以保存用户为例,路径test目录下的cn.zzs.dbcp

@Test    public void save() throws SQLException {
// 创建sql String sql = "insert into demo_user values(null,?,?,?,?,?)"; Connection connection = null; PreparedStatement statement = null; try {
// 获得连接 connection = JDBCUtils.getConnection(); // 开启事务设置非自动提交 connection.setAutoCommit(false); // 获得Statement对象 statement = connection.prepareStatement(sql); // 设置参数 statement.setString(1, "zzf003"); statement.setInt(2, 18); statement.setDate(3, new Date(System.currentTimeMillis())); statement.setDate(4, new Date(System.currentTimeMillis())); statement.setBoolean(5, false); // 执行 statement.executeUpdate(); // 提交事务 connection.commit(); } finally {
// 释放资源 JDBCUtils.release(connection, statement, null); } }

配置文件详解

这部分内容从网上参照过来,同样的内容发的到处都是,暂时没找到出处。因为内容太过杂乱,而且最新版本更新了不少内容,所以我花了好大功夫才改好,后面找到出处再补上参考资料吧。

基本连接属性

注意,这里在url后面拼接了多个参数用于避免乱码、时区报错问题。 补充下,如果不想加入时区的参数,可以在mysql命令窗口执行如下命令:set global time_zone='+8:00'

driverClassName=com.mysql.cj.jdbc.Driverurl=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=trueusername=rootpassword=root

连接池大小参数

这几个参数都比较常用,具体设置多少需根据项目调整。

#-------------连接池大小和连接超时参数--------------------------------#初始化连接数量:连接池启动时创建的初始化连接数量#默认为0initialSize=0#最大活动连接数量:连接池在同一时间能够分配的最大活动连接的数量, 如果设置为负数则表示不限制#默认为8maxTotal=8#最大空闲连接:连接池中容许保持空闲状态的最大连接数量,超过的空闲连接将被释放,如果设置为负数表示不限制#默认为8maxIdle=8#最小空闲连接:连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接,如果设置为0则不创建#注意:timeBetweenEvictionRunsMillis为正数时,这个参数才能生效。#默认为0minIdle=0#最大等待时间#当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计数),超过时间则抛出异常,如果设置为<=0表示无限等待#默认-1maxWaitMillis=-1#连接池创建的连接的默认的数据库名,如果是使用DBCP的XA连接必须设置,不然注册不了多个资源管理器#defaultCatalog=github_demo#连接池创建的连接的默认的schema。如果是mysql,这个设置没什么用。#defaultSchema=github_demo

缓存语句

缓存语句在mysql下建议关闭。

#-------------缓存语句--------------------------------#是否缓存preparedStatement,也就是PSCache。#PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭#默认为falsepoolPreparedStatements=false#缓存PreparedStatements的最大个数#默认为-1#注意:poolPreparedStatements为true时,这个参数才有效maxOpenPreparedStatements=-1#缓存read-only和auto-commit状态。设置为true的话,所有连接的状态都会是一样的。#默认是truecacheState=true

连接检查参数

针对连接失效和连接泄露的问题,建议开启testWhileIdle,而不是开启testOnReturntestOnBorrow(从性能考虑)。

#-------------连接检查情况--------------------------------#通过SQL查询检测连接,注意必须返回至少一行记录#默认为空。即会调用Connection的isValid和isClosed进行检测#注意:如果是oracle数据库的话,应该改为select 1 from dualvalidationQuery=select 1 from dual#SQL检验超时时间validationQueryTimeout=-1#是否从池中取出连接前进行检验。#默认为truetestOnBorrow=true#是否在归还到池中前进行检验 #默认为falsetestOnReturn=false#是否开启空闲资源回收器。#默认为falsetestWhileIdle=false#空闲资源的检测周期(单位为毫秒)。#默认-1。即空闲资源回收器不工作。timeBetweenEvictionRunsMillis=-1#做空闲资源回收器时,每次的采样数。#默认3,单位毫秒。如果设置为-1,就是对所有连接做空闲监测。numTestsPerEvictionRun=3#资源池中资源最小空闲时间(单位为毫秒),达到此值后将被移除。#默认值1000*60*30 = 30分钟minEvictableIdleTimeMillis=1800000#资源池中资源最小空闲时间(单位为毫秒),达到此值后将被移除。但是会保证minIdle#默认值-1#softMinEvictableIdleTimeMillis=-1#空闲资源回收策略#默认org.apache.commons.pool2.impl.DefaultEvictionPolicy#如果要自定义的话,需要实现EvictionPolicy重写evict方法evictionPolicyClassName=org.apache.commons.pool2.impl.DefaultEvictionPolicy#连接最大存活时间。非正数表示不限制#默认-1maxConnLifetimeMillis=-1#当达到maxConnLifetimeMillis被关闭时,是否打印相关消息#默认true#注意:maxConnLifetimeMillis设置为正数时,这个参数才有效logExpiredConnections=true

事务相关参数

这里的参数主要和事务相关,一般默认就行。

#-------------事务相关的属性--------------------------------#连接池创建的连接的默认的auto-commit状态#默认为空,由驱动决定defaultAutoCommit=true#连接池创建的连接的默认的read-only状态。#默认值为空,由驱动决定defaultReadOnly=false#连接池创建的连接的默认的TransactionIsolation状态#可用值为下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE#默认值为空,由驱动决定defaultTransactionIsolation=REPEATABLE_READ#归还连接时是否设置自动提交为true#默认trueautoCommitOnReturn=true#归还连接时是否设置回滚事务#默认truerollbackOnReturn=true

连接泄漏回收参数

当我们从连接池获得了连接对象,但因为疏忽或其他原因没有close,这个时候这个连接对象就是一个泄露资源。通过配置以下参数可以回收这部分对象。

#-------------连接泄漏回收参数--------------------------------#当未使用的时间超过removeAbandonedTimeout时,是否视该连接为泄露连接并删除(当getConnection()被调用时检测)#默认为false#注意:这个机制在(getNumIdle() < 2) and (getNumActive() > (getMaxActive() - 3))时被触发removeAbandonedOnBorrow=false#当未使用的时间超过removeAbandonedTimeout时,是否视该连接为泄露连接并删除(空闲evictor检测)#默认为false#注意:当空闲资源回收器开启才生效removeAbandonedOnMaintenance=false#泄露的连接可以被删除的超时值, 单位秒#默认为300removeAbandonedTimeout=300#标记当Statement或连接被泄露时是否打印程序的stack traces日志。#默认为falselogAbandoned=true#这个不是很懂#默认为falseabandonedUsageTracking=false

其他

这部分参数比较少用。

#-------------其他--------------------------------#是否使用快速失败机制#默认为空,由驱动决定fastFailValidation=false#当使用快速失败机制时,设置触发的异常码#多个code用","隔开#disconnectionSqlCodes#borrow连接的顺序#默认truelifo=true#每个连接创建时执行的语句#connectionInitSqls=#连接参数:例如username、password、characterEncoding等都可以在这里设置#多个参数用";"隔开#connectionProperties=#指定数据源的jmx名。注意,配置了才能注册MBeanjmxName=cn.zzs.jmx:type=BasicDataSource,name=zzs001#查询超时时间#默认为空,即根据驱动设置#defaultQueryTimeout=#控制PoolGuard是否容许获取底层连接#默认为falseaccessToUnderlyingConnectionAllowed=false#如果容许则可以使用下面的方式来获取底层物理连接:#    Connection conn = ds.getConnection();#    Connection dconn = ((DelegatingConnection) conn).getInnermostDelegate();#    ...#    conn.close();

源码分析

注意:考虑篇幅和可读性,以下代码经过删减,仅保留所需部分。

创建数据源和连接池

研究之前,先来看下BasicDataSourceUML图:

BasicDataSource的UML图

这里介绍下这几个类的作用:

类名 描述
BasicDataSource 用于满足基本数据库操作需求的数据源
BasicManagedDataSource BasicDataSource的子类,用于创建支持XA事务或JTA事务的连接
PoolingDataSource BasicDataSource中实际调用的数据源,可以说BasicDataSource只是封装了PoolingDataSource
ManagedDataSource PoolingDataSource的子类,用于支持XA事务或JTA事务的连接。是BasicManagedDataSource中实际调用的数据源,可以说BasicManagedDataSource只是封装了ManagedDataSource

另外,为了支持JNDIDBCP也提供了相应的类。

类名 描述
InstanceKeyDataSource 用于支持JDNI环境的数据源
PerUserPoolDataSource InstanceKeyDataSource的子类,针对每个用户会单独分配一个连接池,每个连接池可以设置不同属性。例如以下需求,相比user,admin可以创建更多地连接以保证
SharedPoolDataSource InstanceKeyDataSource的子类,不同用户共享一个连接池

本文的源码分析仅会涉及到BasicDataSource(包含它封装的PoolingDataSource),其他的数据源暂时不扩展。

BasicDataSource.getConnection()

BasicDataSourceFactory.createDataSource(Properties)只是简单地new了一个BasicDataSource对象并初始化配置参数,此时真正的数据源(PoolingDataSource)以及连接池(GenericObjectPool)并没有创建,而创建的时机为我们第一次调用getConnection()的时候,如下:

public Connection getConnection() throws SQLException {
return createDataSource().getConnection(); }

但是,当我们设置了 initialSize > 0,则在BasicDataSourceFactory.createDataSource(Properties)时就会完成数据源和连接池的初始化。感谢的指正。

当然,过程都是相同的,只是时机不一样。下面从BasicDataSourcecreateDataSource()方法开始分析。

BasicDataSource.createDataSource()

这个方法会创建数据源和连接池,整个过程可以概括为以下几步:

  1. 注册MBean,用于支持JMX
  2. 创建连接池对象GenericObjectPool<PoolableConnection>
  3. 创建数据源对象PoolingDataSource<PoolableConnection>
  4. 初始化连接数;
  5. 开启空闲资源回收线程(如果设置timeBetweenEvictionRunsMillis为正数)。
protected DataSource createDataSource() throws SQLException {
if(closed) {
throw new SQLException("Data source is closed"); } if(dataSource != null) {
return dataSource; } synchronized(this) {
if(dataSource != null) {
return dataSource; } // 注册MBean,用于支持JMX,这方面的内容不在这里扩展 jmxRegister(); // 创建原生Connection工厂:本质就是持有数据库驱动对象和几个连接参数 final ConnectionFactory driverConnectionFactory = createConnectionFactory(); // 将driverConnectionFactory包装成池化Connection工厂 PoolableConnectionFactory poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory); // 设置PreparedStatements缓存(其实在这里可以发现,上面创建池化工厂时就设置了缓存,这里没必要再设置一遍) poolableConnectionFactory.setPoolStatements(poolPreparedStatements); poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements); // 创建数据库连接池对象GenericObjectPool,用于管理连接 // BasicDataSource将持有GenericObjectPool对象 createConnectionPool(poolableConnectionFactory); // 创建PoolingDataSource对象 // 该对象持有GenericObjectPool对象的引用 DataSource newDataSource = createDataSourceInstance(); newDataSource.setLogWriter(logWriter); // 根据我们设置的initialSize创建初始连接 for(int i = 0; i < initialSize; i++) {
connectionPool.addObject(); } // 开启连接池的evictor线程 startPoolMaintenance(); // 最后BasicDataSource将持有上面创建的PoolingDataSource对象 dataSource = newDataSource; return dataSource; } }

以上方法涉及到几个类,这里再补充下UML图。

GenericObjectPool的UML图

类名 描述
DriverConnectionFactory 用于生成原生的Connection对象
PoolableConnectionFactory 用于生成池化的Connection对象,持有ConnectionFactory对象的引用
GenericObjectPool 数据库连接池,用于管理连接。持有PoolableConnectionFactory对象的引用

获取连接对象

上面已经大致分析了数据源和连接池对象的获取过程,接下来研究下连接对象的获取。在此之前先了解下DBCP中几个Connection实现类。

DelegatingConnection的UML图

类名 描述
DelegatingConnection Connection实现类,是以下几个类的父类
PoolingConnection 用于包装原生的Connection,支持缓存prepareStatementprepareCall
PoolableConnection 用于包装原生的PoolingConnection(如果没有开启poolPreparedStatements,则包装的只是原生Connection),调用close()时只是将连接还给连接池
PoolableManagedConnection PoolableConnection的子类,用于包装ManagedConnection,支持JTAXA事务
ManagedConnection 用于包装原生的Connection,支持JTAXA事务
PoolGuardConnectionWrapper 用于包装PoolableConnection,当accessToUnderlyingConnectionAllowed才能获取底层连接对象。我们获取到的就是这个对象

另外,这里先概括下获得连接的整个过程:

  1. 如果设置了removeAbandonedOnBorrow,达到条件会进行检测;
  2. 从连接池中获取连接,如果没有就通过工厂创建(通过DriverConnectionFactory创建原生对象,再通过PoolableConnectionFactory包装为池化对象);
  3. 通过工厂重新初始化连接对象;
  4. 如果设置了testOnBorrow或者testOnCreate,会通过工厂校验连接有效性;
  5. 使用PoolGuardConnectionWrapper包装连接对象,并返回给客户端

PoolingDataSource.getConnection()

前面已经说过,BasicDataSource本质上是调用PoolingDataSource的方法来获取连接,所以这里从PoolingDataSource.getConnection()开始研究。

以下代码可知,该方法会从连接池中“借出”连接。

public Connection getConnection() throws SQLException {
// 这个泛型C指的是PoolableConnection对象 // 调用的是GenericObjectPool的方法返回PoolableConnection对象,这个方法后面会展开 final C conn = pool.borrowObject(); if (conn == null) {
return null; } // 包装PoolableConnection对象,当accessToUnderlyingConnectionAllowed为true时,可以使用底层连接 return new PoolGuardConnectionWrapper<>(conn); }

GenericObjectPool.borrowObject()

GenericObjectPool是一个很简练的类,里面涉及到的属性设置和锁机制都涉及得非常巧妙。

// 存放着连接池所有的连接对象(但不包含已经释放的)    private final Map
, PooledObject
> allObjects = new ConcurrentHashMap<>(); // 存放着空闲连接对象的阻塞队列 private final LinkedBlockingDeque
> idleObjects; // 为n>1表示当前有n个线程正在创建新连接对象 private long makeObjectCount = 0; // 创建连接对象时所用的锁 private final Object makeObjectCountLock = new Object(); // 连接对象创建总数量 private final AtomicLong createCount = new AtomicLong(0); public T borrowObject() throws Exception {
// 如果我们设置了连接获取等待时间,“借出”过程就必须在指定时间内完成 return borrowObject(getMaxWaitMillis()); } public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
// 校验连接池是否打开状态 assertOpen(); // 如果设置了removeAbandonedOnBorrow,达到触发条件是会遍历所有连接,未使用时长超过removeAbandonedTimeout的将被释放掉(一般可以检测出泄露连接) final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && (getNumIdle() < 2) && (getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac); } PooledObject
p = null; // 连接数达到maxTotal是否阻塞等待 final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; final long waitTime = System.currentTimeMillis(); // 如果获取的连接对象为空,会再次进入获取 while (p == null) { create = false; // 获取空闲队列的第一个元素,如果为空就试图创建新连接 p = idleObjects.pollFirst(); if (p == null) { // 后面分析这个方法 p = create(); if (p != null) { create = true; } } // 连接数达到maxTotal且暂时没有空闲连接,这时需要阻塞等待,直到获得空闲队列中的连接或等待超时 if (blockWhenExhausted) { if (p == null) { if (borrowMaxWaitMillis < 0) { // 无限等待 p = idleObjects.takeFirst(); } else { // 等待maxWaitMillis p = idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); } } // 这个时候还是没有就只能抛出异常 if (p == null) { throw new NoSuchElementException( "Timeout waiting for idle object"); } } else { if (p == null) { throw new NoSuchElementException("Pool exhausted"); } } // 如果连接处于空闲状态,会修改连接的state、lastBorrowTime、lastUseTime、borrowedCount等,并返回true if (!p.allocate()) { p = null; } if (p != null) { // 利用工厂重新初始化连接对象,这里会去校验连接存活时间、设置lastUsedTime、及其他初始参数 try { factory.activateObject(p); } catch (final Exception e) { try { destroy(p); } catch (final Exception e1) { // Ignore - activation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( "Unable to activate object"); nsee.initCause(e); throw nsee; } } // 根据设置的参数,判断是否检测连接有效性 if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) { boolean validate = false; Throwable validationThrowable = null; try { // 这里会去校验连接的存活时间是否超过maxConnLifetimeMillis,以及通过SQL去校验执行时间 validate = factory.validateObject(p); } catch (final Throwable t) { PoolUtils.checkRethrow(t); validationThrowable = t; } // 如果校验不通过,会释放该对象 if (!validate) { try { destroy(p); destroyedByBorrowValidationCount.incrementAndGet(); } catch (final Exception e) { // Ignore - validation failure is more important } p = null; if (create) { final NoSuchElementException nsee = new NoSuchElementException( "Unable to validate object"); nsee.initCause(validationThrowable); throw nsee; } } } } } // 更新borrowedCount、idleTimes和waitTimes updateStatsBorrow(p, System.currentTimeMillis() - waitTime); return p.getObject(); }

GenericObjectPool.create()

这里在创建连接对象时采用的锁机制非常值得学习,简练且高效。

private PooledObject
create() throws Exception {
int localMaxTotal = getMaxTotal(); if (localMaxTotal < 0) {
localMaxTotal = Integer.MAX_VALUE; } final long localStartTimeMillis = System.currentTimeMillis(); final long localMaxWaitTimeMillis = Math.max(getMaxWaitMillis(), 0); // 创建标识: // - TRUE: 调用工厂创建返回对象 // - FALSE: 直接返回null // - null: 继续循环 Boolean create = null; while (create == null) {
synchronized (makeObjectCountLock) {
final long newCreateCount = createCount.incrementAndGet(); if (newCreateCount > localMaxTotal) {
// 当前池已经达到maxTotal,或者有另外一个线程正在试图创建一个新的连接使之达到容量极限 createCount.decrementAndGet(); if (makeObjectCount == 0) {
// 连接池确实已达到容量极限 create = Boolean.FALSE; } else {
// 当前另外一个线程正在试图创建一个新的连接使之达到容量极限,此时需要等待 makeObjectCountLock.wait(localMaxWaitTimeMillis); } } else {
// 当前连接池容量未到达极限,可以继续创建连接对象 makeObjectCount++; create = Boolean.TRUE; } } // 当达到maxWaitTimeMillis时不创建连接对象,直接退出循环 if (create == null && (localMaxWaitTimeMillis > 0 && System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) {
create = Boolean.FALSE; } } if (!create.booleanValue()) {
return null; } final PooledObject
p; try {
// 调用工厂创建对象,后面对这个方法展开分析 p = factory.makeObject(); } catch (final Throwable e) {
createCount.decrementAndGet(); throw e; } finally {
synchronized (makeObjectCountLock) {
// 创建标识-1 makeObjectCount--; // 唤醒makeObjectCountLock锁住的对象 makeObjectCountLock.notifyAll(); } } final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getLogAbandoned()) {
p.setLogAbandoned(true); // TODO: in 3.0, this can use the method defined on PooledObject if (p instanceof DefaultPooledObject
) {
((DefaultPooledObject
) p).setRequireFullStackTrace(ac.getRequireFullStackTrace()); } } // 连接数量+1 createdCount.incrementAndGet(); // 将创建的对象放入allObjects allObjects.put(new IdentityWrapper<>(p.getObject()), p); return p; }

PoolableConnectionFactory.makeObject()

public PooledObject
makeObject() throws Exception {
// 创建原生的Connection对象 Connection conn = connectionFactory.createConnection(); if (conn == null) {
throw new IllegalStateException("Connection factory returned null from createConnection"); } try {
// 执行我们设置的connectionInitSqls initializeConnection(conn); } catch (final SQLException sqle) {
// Make sure the connection is closed try {
conn.close(); } catch (final SQLException ignore) {
// ignore } // Rethrow original exception so it is visible to caller throw sqle; } // 连接索引+1 final long connIndex = connectionIndex.getAndIncrement(); // 如果设置了poolPreparedStatements,则创建包装连接为PoolingConnection对象 if (poolStatements) {
conn = new PoolingConnection(conn); final GenericKeyedObjectPoolConfig
config = new GenericKeyedObjectPoolConfig<>(); config.setMaxTotalPerKey(-1); config.setBlockWhenExhausted(false); config.setMaxWaitMillis(0); config.setMaxIdlePerKey(1); config.setMaxTotal(maxOpenPreparedStatements); if (dataSourceJmxObjectName != null) {
final StringBuilder base = new StringBuilder(dataSourceJmxObjectName.toString()); base.append(Constants.JMX_CONNECTION_BASE_EXT); base.append(Long.toString(connIndex)); config.setJmxNameBase(base.toString()); config.setJmxNamePrefix(Constants.JMX_STATEMENT_POOL_PREFIX); } else {
config.setJmxEnabled(false); } final PoolingConnection poolingConn = (PoolingConnection) conn; final KeyedObjectPool
stmtPool = new GenericKeyedObjectPool<>( poolingConn, config); poolingConn.setStatementPool(stmtPool); poolingConn.setCacheState(cacheState); } // 用于注册连接到JMX ObjectName connJmxName; if (dataSourceJmxObjectName == null) {
connJmxName = null; } else {
connJmxName = new ObjectName( dataSourceJmxObjectName.toString() + Constants.JMX_CONNECTION_BASE_EXT + connIndex); } // 创建PoolableConnection对象 final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, disconnectionSqlCodes, fastFailValidation); pc.setCacheState(cacheState); // 包装成连接池所需的对象 return new DefaultPooledObject<>(pc); }

空闲对象回收器Evictor

以上基本已分析完连接对象的获取过程,下面再研究下空闲对象回收器。前面已经讲到当创建完数据源对象时会开启连接池的evictor线程,所以我们从BasicDataSource.startPoolMaintenance()开始分析。

BasicDataSource.startPoolMaintenance()

前面说过timeBetweenEvictionRunsMillis为非正数时不会开启开启空闲对象回收器,从以下代码可以理解具体逻辑。

protected void startPoolMaintenance() {
// 只有timeBetweenEvictionRunsMillis为正数,才会开启空闲对象回收器 if (connectionPool != null && timeBetweenEvictionRunsMillis > 0) {
connectionPool.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); } }

BaseGenericObjectPool.setTimeBetweenEvictionRunsMillis(long)

这个BaseGenericObjectPool是上面说到的GenericObjectPool的父类。

public final void setTimeBetweenEvictionRunsMillis(            final long timeBetweenEvictionRunsMillis) {
// 设置回收线程运行间隔时间 this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; // 继续调用本类的方法,下面继续进入方法分析 startEvictor(timeBetweenEvictionRunsMillis); }

BaseGenericObjectPool.startEvictor(long)

这里会去定义一个Evictor对象,这个其实是一个Runnable对象,后面会讲到。

final void startEvictor(final long delay) {
synchronized (evictionLock) {
if (null != evictor) {
EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS); evictor = null; evictionIterator = null; } // 创建回收器任务,并执行定时调度 if (delay > 0) {
evictor = new Evictor(); EvictionTimer.schedule(evictor, delay, delay); } } }

EvictionTimer.schedule(Evictor, long, long)

DBCP是使用ScheduledThreadPoolExecutor来实现回收器的定时检测。 涉及到ThreadPoolExecutorJDK自带的api,这里不再深入分析线程池如何实现定时调度。感兴趣的朋友可以复习下常用的几款线程池。

static synchronized void schedule(            final BaseGenericObjectPool
.Evictor task, final long delay, final long period) if (null == executor) {
// 创建线程池,队列为DelayedWorkQueue,corePoolSize为1,maximumPoolSize为无限大 executor = new ScheduledThreadPoolExecutor(1, new EvictorThreadFactory()); // 当任务被取消的同时从等待队列中移除 executor.setRemoveOnCancelPolicy(true); } // 设置任务定时调度 final ScheduledFuture
scheduledFuture = executor.scheduleWithFixedDelay(task, delay, period, TimeUnit.MILLISECONDS); task.setScheduledFuture(scheduledFuture); }

BaseGenericObjectPool.Evictor

EvictorBaseGenericObjectPool的内部类,实现了Runnable接口,这里看下它的run方法。

class Evictor implements Runnable {
private ScheduledFuture
scheduledFuture; @Override public void run() {
final ClassLoader savedClassLoader = Thread.currentThread().getContextClassLoader(); try {
// 确保回收器使用的类加载器和工厂对象的一样 if (factoryClassLoader != null) {
final ClassLoader cl = factoryClassLoader.get(); if (cl == null) {
cancel(); return; } Thread.currentThread().setContextClassLoader(cl); } try {
// 回收符合条件的对象,后面继续扩展 evict(); } catch(final Exception e) {
swallowException(e); } catch(final OutOfMemoryError oome) {
// Log problem but give evictor thread a chance to continue // in case error is recoverable oome.printStackTrace(System.err); } try {
// 确保最小空闲对象 ensureMinIdle(); } catch (final Exception e) {
swallowException(e); } } finally {
Thread.currentThread().setContextClassLoader(savedClassLoader); } } void setScheduledFuture(final ScheduledFuture
scheduledFuture) {
this.scheduledFuture = scheduledFuture; } void cancel() {
scheduledFuture.cancel(false); } }

GenericObjectPool.evict()

这里的回收过程包括以下四道校验:

  1. 按照evictionPolicy校验idleSoftEvictTimeidleEvictTime

  2. 利用工厂重新初始化样本,这里会校验maxConnLifetimeMillistestWhileIdle为true);

  3. 校验maxConnLifetimeMillisvalidationQueryTimeouttestWhileIdle为true);

  4. 校验所有连接的未使用时间是否超过removeAbandonedTimeoutremoveAbandonedOnMaintenance为true)。

public void evict() throws Exception {
// 校验当前连接池是否关闭 assertOpen(); if (idleObjects.size() > 0) {
PooledObject
underTest = null; // 介绍参数时已经讲到,这个evictionPolicy我们可以自定义 final EvictionPolicy
evictionPolicy = getEvictionPolicy(); synchronized (evictionLock) {
final EvictionConfig evictionConfig = new EvictionConfig( getMinEvictableIdleTimeMillis(), getSoftMinEvictableIdleTimeMillis(), getMinIdle()); final boolean testWhileIdle = getTestWhileIdle(); // 获取我们指定的样本数,并开始遍历 for (int i = 0, m = getNumTests(); i < m; i++) {
if (evictionIterator == null || !evictionIterator.hasNext()) {
evictionIterator = new EvictionIterator(idleObjects); } if (!evictionIterator.hasNext()) {
// Pool exhausted, nothing to do here return; } try {
underTest = evictionIterator.next(); } catch (final NoSuchElementException nsee) {
// 当前样本正被另一个线程借出 i--; evictionIterator = null; continue; } // 判断如果样本是空闲状态,设置为EVICTION状态 // 如果不是,说明另一个线程已经借出了这个样本 if (!underTest.startEvictionTest()) {
i--; continue; } boolean evict; try {
// 调用回收策略来判断是否回收该样本,按照默认策略,以下情况都会返回true: // 1. 样本空闲时间大于我们设置的idleSoftEvictTime,且当前池中空闲连接数量>minIdle // 2. 样本空闲时间大于我们设置的idleEvictTime evict = evictionPolicy.evict(evictionConfig, underTest, idleObjects.size()); } catch (final Throwable t) {
PoolUtils.checkRethrow(t); swallowException(new Exception(t)); evict = false; } // 如果需要回收,则释放这个样本 if (evict) {
destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } else {
// 如果设置了testWhileIdle,会 if (testWhileIdle) {
boolean active = false; try {
// 利用工厂重新初始化样本,这里会校验maxConnLifetimeMillis factory.activateObject(underTest); active = true; } catch (final Exception e) {
// 抛出异常标识校验不通过,释放样本 destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } if (active) {
// 接下来会校验maxConnLifetimeMillis和validationQueryTimeout if (!factory.validateObject(underTest)) {
destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } else {
try {
// 这里会将样本rollbackOnReturn、autoCommitOnReturn等 factory.passivateObject(underTest); } catch (final Exception e) {
destroy(underTest); destroyedByEvictorCount.incrementAndGet(); } } } } // 如果状态为EVICTION或EVICTION_RETURN_TO_HEAD,修改为IDLE if (!underTest.endEvictionTest(idleObjects)) {
//空 } } } } } // 校验所有连接的未使用时间是否超过removeAbandonedTimeout final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
removeAbandoned(ac); } }

以上已基本研究完数据源创建、连接对象获取和空闲资源回收器,后续有空再做补充。

通过JNDI获取数据源对象

需求

本文测试使用JNDI获取PerUserPoolDataSourceSharedPoolDataSource对象,选择使用tomcat 9.0.21作容器。

如果之前没有接触过JNDI,并不会影响下面例子的理解,其实可以理解为像springbean配置和获取。

源码分析时已经讲到,除了我们熟知的BasicDataSourceDBCP还提供了通过JDNI获取数据源,如下表。

类名 描述
InstanceKeyDataSource 用于支持JDNI环境的数据源,是以下两个类的父类
PerUserPoolDataSource InstanceKeyDataSource的子类,针对每个用户会单独分配一个连接池,每个连接池可以设置不同属性。例如以下需求,相比user,admin可以创建更多地连接以保证
SharedPoolDataSource InstanceKeyDataSource的子类,不同用户共享一个连接池

引入依赖

本文在前面例子的基础上增加以下依赖,因为是web项目,所以打包方式为war

javax.servlet
jstl
1.2
provided
javax.servlet
javax.servlet-api
3.1.0
provided
javax.servlet.jsp
javax.servlet.jsp-api
2.2.1
provided

编写context.xml

webapp文件下创建目录META-INF,并创建context.xml文件。这里面的每个resource节点都是我们配置的对象,类似于springbean节点。其中bean/DriverAdapterCPDS这个对象需要被另外两个使用到。

编写web.xml

web-app节点下配置资源引用,每个resource-env-ref指向了我们配置好的对象。

Test DriverAdapterCPDS
bean/DriverAdapterCPDS
org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS
Test SharedPoolDataSource
bean/SharedPoolDataSourceFactory
org.apache.commons.dbcp2.datasources.SharedPoolDataSource
Test erUserPoolDataSource
bean/erUserPoolDataSourceFactory
org.apache.commons.dbcp2.datasources.erUserPoolDataSource

编写jsp

因为需要在web环境中使用,如果直接建类写个main方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp来测试吧(这是从tomcat官网参照的例子)。

    <%          // 获得名称服务的上下文对象        Context initCtx = new InitialContext();        Context envCtx = (Context)initCtx.lookup("java:comp/env/");                // 查找指定名字的对象        DataSource ds = (DataSource)envCtx.lookup("bean/SharedPoolDataSourceFactory");                DataSource ds2 = (DataSource)envCtx.lookup("bean/PerUserPoolDataSourceFactory");                // 获取连接        Connection conn = ds.getConnection("root","root");        System.out.println("conn" + conn);         Connection conn2 = ds2.getConnection("zzf","zzf");        System.out.println("conn2" + conn2);                 // ... 使用连接操作数据库,以及释放资源 ...        conn.close();        conn2.close();    %>

测试结果

打包项目在tomcat9上运行,访问 http://localhost:8080/DBCP-demo/testInstanceKeyDataSource.jsp ,控制台打印如下内容:

conn=1971654708, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=root@localhost, MySQL Connector/Jconn2=128868782, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=zzf@localhost, MySQL Connector/J

使用DBCP测试两阶段提交

前面源码分析已经讲到,以下类用于支持JTA事务。本文将介绍如何使用DBCP来实现JTA事务两阶段提交(当然,实际项目并不支持使用2PC,因为性能开销太大)。

类名 描述
BasicManagedDataSource BasicDataSource的子类,用于创建支持XA事务或JTA事务的连接
ManagedDataSource PoolingDataSource的子类,用于支持XA事务或JTA事务的连接。是BasicManagedDataSource中实际调用的数据源,可以说BasicManagedDataSource只是封装了ManagedDataSource

准备工作

因为测试例子使用的是mysql,使用XA事务需要开启支持。注意,mysql只有innoDB引擎才支持(另外,XA事务和常规事务是互斥的,如果开启了XA事务,其他线程进来即使只读也是不行的)。

SHOW VARIABLES LIKE '%xa%' -- 查看XA事务是否开启SET innodb_support_xa = ON -- 开启XA事务

除了原来的github_demo数据库,我另外建了一个test数据库,简单地模拟两个数据库。

mysql的XA事务使用

测试之前,这里简单回顾下直接使用sql操作XA事务的过程,将有助于对以下内容的理解:

XA START 'my_test_xa'; -- 启动一个xid为my_test_xa的事务,并使之为active状态UPDATE github_demo.demo_user SET deleted = 1 WHERE id = '1'; -- 事务中的语句XA END 'my_test_xa'; -- 把事务置为idle状态XA PREPARE 'my_test_xa'; -- 把事务置为prepare状态XA COMMIT 'my_test_xa'; -- 提交事务XA ROLLBACK 'my_test_xa'; -- 回滚事务XA RECOVER; -- 查看处于prepare状态的事务列表

引入依赖

在入门例子的基础上,增加以下依赖,本文采用第三方atomikos的实现。

javax.transaction
jta
1.1
com.atomikos
transactions-jdbc
3.9.3

获取BasicManagedDataSource

这里千万记得要设置DefaultCatalog,否则当前事务中注册不同资源管理器时,可能都会被当成同一个资源管理器而拒绝注册并报错,因为这个问题,花了我好长时间才解决。

public BasicManagedDataSource getBasicManagedDataSource(            TransactionManager transactionManager,             String url,             String username,             String password) {
BasicManagedDataSource basicManagedDataSource = new BasicManagedDataSource(); basicManagedDataSource.setTransactionManager(transactionManager); basicManagedDataSource.setUrl(url); basicManagedDataSource.setUsername(username); basicManagedDataSource.setPassword(password); basicManagedDataSource.setDefaultAutoCommit(false); basicManagedDataSource.setXADataSource("com.mysql.cj.jdbc.MysqlXADataSource"); return basicManagedDataSource; } @Test public void test01() throws Exception {
// 获得事务管理器 TransactionManager transactionManager = new UserTransactionManager(); // 获取第一个数据库的数据源 BasicManagedDataSource basicManagedDataSource1 = getBasicManagedDataSource( transactionManager, "jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true", "root", "root"); // 注意,这一步非常重要 basicManagedDataSource1.setDefaultCatalog("github_demo"); // 获取第二个数据库的数据源 BasicManagedDataSource basicManagedDataSource2 = getBasicManagedDataSource( transactionManager, "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true", "zzf", "zzf"); // 注意,这一步非常重要 basicManagedDataSource1.setDefaultCatalog("test"); }

编写两阶段提交的代码

通过运行代码可以发现,当数据库1和2的操作都成功,才会提交,只要其中一个数据库执行失败,两个操作都会回滚。

@Test    public void test01() throws Exception {
Connection connection1 = null; Statement statement1 = null; Connection connection2 = null; Statement statement2 = null; transactionManager.begin(); try {
// 获取连接并进行数据库操作,这里会将会将XAResource注册到当前线程的XA事务对象 /** * XA START xid1;-- 启动一个事务,并使之为active状态 */ connection1 = basicManagedDataSource1.getConnection(); statement1 = connection1.createStatement(); /** * update github_demo.demo_user set deleted = 1 where id = '1'; -- 事务中的语句 */ boolean result1 = statement1.execute("update github_demo.demo_user set deleted = 1 where id = '1'"); System.out.println(result1); /** * XA START xid2;-- 启动一个事务,并使之为active状态 */ connection2 = basicManagedDataSource2.getConnection(); statement2 = connection2.createStatement(); /** * update test.demo_user set deleted = 1 where id = '1'; -- 事务中的语句 */ boolean result2 = statement2.execute("update test.demo_user set deleted = 1 where id = '1'"); System.out.println(result2); /** * 当这执行以下语句: * XA END xid1; -- 把事务置为idle状态 * XA PREPARE xid1; -- 把事务置为prepare状态 * XA END xid2; -- 把事务置为idle状态 * XA PREPARE xid2; -- 把事务置为prepare状态 * XA COMMIT xid1; -- 提交事务 * XA COMMIT xid2; -- 提交事务 */ transactionManager.commit(); } catch(Exception e) {
e.printStackTrace(); } finally {
statement1.close(); statement2.close(); connection1.close(); connection2.close(); } }

相关源码请移步:https://github.com/ZhangZiSheng001/dbcp-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12003922.html

你可能感兴趣的文章
杭电ACM——fatmouse's speed(DP)
查看>>
杭电ACM——毛毛虫(DP)
查看>>
杭电ACM——humble numbers(DP)
查看>>
杭电ACM——6467,简单数学题(思维)
查看>>
杭电ACM——天上掉馅饼(DP)
查看>>
杭电ACM——1086,You can Solve a Geometry Problem too(思维)
查看>>
杭电ACM——2057,A + B Again(思维)
查看>>
codeforces——1097B,Petr and a Combination Lock(搜索)
查看>>
杭电ACM——2064,汉诺塔III(递推)
查看>>
杭电ACM——2065,"红色病毒"问题(思维)
查看>>
北大ACM——2385,Apple Catching(DP)
查看>>
杭电AM——2072,单词数(暴力)
查看>>
杭电ACM——2073,无限的路(思维)
查看>>
杭电ACM——2069,Coin Change(DP)
查看>>
杭电ACM——2074,叠筐
查看>>
北大ACM——3616,Milking Time(DP)
查看>>
杭电ACM——2076,夹角有多大
查看>>
牛客练习赛43——B Tachibana Kanade Loves Probability(暴力,思维)
查看>>
牛客第十七届上海大学程序设计春季联赛——E CSL 的魔法(贪心)
查看>>
杭电ACM——1028,Ignatius and the Princess III(母函数)
查看>>