DBCP
用于建立和管理链接,利用“池”的方式复用链接减小资源开销,和其余链接池同样,也具备链接数控制、链接有效性检测、链接泄露控制、缓存语句等功能。目前,tomcat
自带的链接池就是DBCP
,Spring开发组也推荐使用DBCP
,阿里的druid
也是参照DBCP
开发出来的。html
DBCP
除了咱们熟知的使用方式外,还支持经过JNDI
获取数据源,并支持获取JTA
或XA
事务中用于2PC
(两阶段提交)的链接对象,本文也将以例子说明。java
本文将包含如下内容(由于篇幅较长,可根据须要选择阅读):mysql
DBCP
的使用方法(入门案例说明);DBCP
的配置参数详解;DBCP
主要源码分析;DBCP
其余特性的使用方法,如JNDI
和JTA
支持。使用DBCP
链接池获取链接对象,对用户数据进行简单的增删改查。git
JDK
:1.8.0_201github
maven
:3.6.1web
IDE
:eclipse 4.12spring
mysql-connector-java
:8.0.15sql
mysql
:5.7.28数据库
DBCP
:2.6.0apache
编写dbcp.properties
,设置数据库链接参数和链接池基本参数等。
经过BasicDataSourceFactory
加载dbcp.properties
,并得到BasicDataDource
对象。
经过BasicDataDource
对象获取Connection
对象。
使用Connection
对象对用户表进行增删改查。
项目类型Maven Project,打包方式war(其实jar也能够,之因此使用war是为了测试JNDI
)。
<!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- dbcp --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.6.0</version> </dependency> <!-- log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- mysql驱动的jar包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency>
路径resources
目录下,由于是入门例子,这里仅给出数据库链接参数和链接池基本参数,后面源码会对配置参数进行详细说明。另外,数据库sql
脚本也在该目录下。
#链接基本属性 driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true username=root password=root #-------------链接池大小和链接超时参数-------------------------------- #初始化链接数量:链接池启动时建立的初始化链接数量 #默认为0 initialSize=0 #最大活动链接数量:链接池在同一时间可以分配的最大活动链接的数量, 若是设置为负数则表示不限制 #默认为8 maxTotal=8 #最大空闲链接:链接池中允许保持空闲状态的最大链接数量,超过的空闲链接将被释放,若是设置为负数表示不限制 #默认为8 maxIdle=8 #最小空闲链接:链接池中允许保持空闲状态的最小链接数量,低于这个数量将建立新的链接,若是设置为0则不建立 #注意:timeBetweenEvictionRunsMillis为正数时,这个参数才能生效。 #默认为0 minIdle=0 #最大等待时间 #当没有可用链接时,链接池等待链接被归还的最大时间(以毫秒计数),超过期间则抛出异常,若是设置为<=0表示无限等待 #默认-1 maxWaitMillis=-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() { // 建立sql String sql = "insert into demo_user values(null,?,?,?,?,?)"; Connection connection = null; PreparedStatement statement = null; try { // 得到链接 connection = JDBCUtils.getConnection(); // 开启事务设置非自动提交 JDBCUtils.startTrasaction(); // 得到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(); // 提交事务 JDBCUtils.commit(); } catch(Exception e) { JDBCUtils.rollback(); log.error("保存用户失败", e); } finally { // 释放资源 JDBCUtils.release(connection, statement, null); } }
这部份内容从网上参照过来,一样的内容发的处处都是,暂时没找到出处。由于内容太过杂乱,并且最新版本更新了很多内容,因此我花了好大功夫才改好,后面找到出处再补上参考资料吧。
注意,这里在url
后面拼接了多个参数用于避免乱码、时区报错问题。 补充下,若是不想加入时区的参数,能够在mysql
命令窗口执行以下命令:set global time_zone='+8:00'
。
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true username=root password=root
这几个参数都比较经常使用,具体设置多少需根据项目调整。
#-------------链接池大小和链接超时参数-------------------------------- #初始化链接数量:链接池启动时建立的初始化链接数量 #默认为0 initialSize=0 #最大活动链接数量:链接池在同一时间可以分配的最大活动链接的数量, 若是设置为负数则表示不限制 #默认为8 maxTotal=8 #最大空闲链接:链接池中允许保持空闲状态的最大链接数量,超过的空闲链接将被释放,若是设置为负数表示不限制 #默认为8 maxIdle=8 #最小空闲链接:链接池中允许保持空闲状态的最小链接数量,低于这个数量将建立新的链接,若是设置为0则不建立 #注意:timeBetweenEvictionRunsMillis为正数时,这个参数才能生效。 #默认为0 minIdle=0 #最大等待时间 #当没有可用链接时,链接池等待链接被归还的最大时间(以毫秒计数),超过期间则抛出异常,若是设置为<=0表示无限等待 #默认-1 maxWaitMillis=-1 #链接池建立的链接的默认的数据库名,若是是使用DBCP的XA链接必须设置,否则注册不了多个资源管理器 #defaultCatalog=github_demo #链接池建立的链接的默认的schema。若是是mysql,这个设置没什么用。 #defaultSchema=github_demo
缓存语句在mysql
下建议关闭。
#-------------缓存语句-------------------------------- #是否缓存preparedStatement,也就是PSCache。 #PSCache对支持游标的数据库性能提高巨大,好比说oracle。在mysql下建议关闭 #默认为false poolPreparedStatements=false #缓存PreparedStatements的最大个数 #默认为-1 #注意:poolPreparedStatements为true时,这个参数才有效 maxOpenPreparedStatements=-1 #缓存read-only和auto-commit状态。设置为true的话,全部链接的状态都会是同样的。 #默认是true cacheState=true
针对链接失效和链接泄露的问题,建议开启testWhileIdle
,而不是开启testOnReturn
或testOnBorrow
(从性能考虑)。
#-------------链接检查状况-------------------------------- #经过SQL查询检测链接,注意必须返回至少一行记录 #默认为空。即会调用Connection的isValid和isClosed进行检测 #注意:若是是oracle数据库的话,应该改成select 1 from dual validationQuery=select 1 from dual #SQL检验超时时间 validationQueryTimeout=-1 #是否从池中取出链接前进行检验。 #默认为true testOnBorrow=true #是否在归还到池中前进行检验 #默认为false testOnReturn=false #是否开启空闲资源回收器。 #默认为false testWhileIdle=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 #链接最大存活时间。非正数表示不限制 #默认-1 maxConnLifetimeMillis=-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 #默认true autoCommitOnReturn=true #归还链接时是否设置回滚事务 #默认true rollbackOnReturn=true
当咱们从链接池得到了链接对象,但由于疏忽或其余缘由没有close
,这个时候这个链接对象就是一个泄露资源。经过配置如下参数能够回收这部分对象。
#-------------链接泄漏回收参数-------------------------------- #当未使用的时间超过removeAbandonedTimeout时,是否视该链接为泄露链接并删除(当getConnection()被调用时检测) #默认为false #注意:这个机制在(getNumIdle() < 2) and (getNumActive() > (getMaxActive() - 3))时被触发 removeAbandonedOnBorrow=false #当未使用的时间超过removeAbandonedTimeout时,是否视该链接为泄露链接并删除(空闲evictor检测) #默认为false #注意:当空闲资源回收器开启才生效 removeAbandonedOnMaintenance=false #泄露的链接能够被删除的超时值, 单位秒 #默认为300 removeAbandonedTimeout=300 #标记当Statement或链接被泄露时是否打印程序的stack traces日志。 #默认为false logAbandoned=true #这个不是很懂 #默认为false abandonedUsageTracking=false
这部分参数比较少用。
#-------------其余-------------------------------- #是否使用快速失败机制 #默认为空,由驱动决定 fastFailValidation=false #当使用快速失败机制时,设置触发的异常码 #多个code用","隔开 #disconnectionSqlCodes #borrow链接的顺序 #默认true lifo=true #每一个链接建立时执行的语句 #connectionInitSqls= #链接参数:例如username、password、characterEncoding等均可以在这里设置 #多个参数用";"隔开 #connectionProperties= #指定数据源的jmx名。注意,配置了才能注册MBean jmxName=cn.zzs.jmx:type=BasicDataSource,name=zzs001 #查询超时时间 #默认为空,即根据驱动设置 #defaultQueryTimeout= #控制PoolGuard是否允许获取底层链接 #默认为false accessToUnderlyingConnectionAllowed=false #若是允许则可使用下面的方式来获取底层物理链接: # Connection conn = ds.getConnection(); # Connection dconn = ((DelegatingConnection) conn).getInnermostDelegate(); # ... # conn.close();
注意:考虑篇幅和可读性,如下代码通过删减,仅保留所需部分。
研究以前,先来看下BasicDataSource
的UML
图:
这里介绍下这几个类的做用:
类名 | 描述 |
---|---|
BasicDataSource |
用于知足基本数据库操做需求的数据源 |
BasicManagedDataSource |
BasicDataSource 的子类,用于建立支持XA 事务或JTA 事务的链接 |
PoolingDataSource |
BasicDataSource 中实际调用的数据源,能够说BasicDataSource 只是封装了PoolingDataSource |
ManagedDataSource |
PoolingDataSource 的子类,用于支持XA 事务或JTA 事务的链接。是BasicManagedDataSource 中实际调用的数据源,能够说BasicManagedDataSource 只是封装了ManagedDataSource |
另外,为了支持JNDI
,DBCP
也提供了相应的类。
类名 | 描述 |
---|---|
InstanceKeyDataSource |
用于支持JDNI 环境的数据源 |
PerUserPoolDataSource |
InstanceKeyDataSource 的子类,针对每一个用户会单独分配一个链接池,每一个链接池能够设置不一样属性。例如如下需求,相比user,admin 能够建立更多地链接以保证 |
SharedPoolDataSource |
InstanceKeyDataSource 的子类,不一样用户共享一个链接池 |
本文的源码分析仅会涉及到BasicDataSource
(包含它封装的PoolingDataSource
),其余的数据源暂时不扩展。
BasicDataSourceFactory
只是简单地new
了一个BasicDataSource
对象并初始化配置参数,此时真正的数据源(PoolingDataSource
)以及链接池(GenericObjectPool<PoolableConnection>
)并无建立,而建立的时机为咱们第一次调用getConnection()
的时候。所以,本文直接从BasicDataSource
的getConnection()
方法开始分析。
public Connection getConnection() throws SQLException { return createDataSource().getConnection(); }
这个方法会建立数据源和链接池,整个过程能够归纳为如下几步:
MBean
,用于支持JMX
;GenericObjectPool<PoolableConnection>
;PoolingDataSource<PoolableConnection>
;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
图。
类名 | 描述 |
---|---|
DriverConnectionFactory |
用于生成原生的Connection对象 |
PoolableConnectionFactory |
用于生成池化的Connection对象,持有ConnectionFactory 对象的引用 |
GenericObjectPool |
数据库链接池,用于管理链接。持有PoolableConnectionFactory 对象的引用 |
上面已经大体分析了数据源和链接池对象的获取过程,接下来研究下链接对象的获取。在此以前先了解下DBCP
中几个Connection
实现类。
类名 | 描述 |
---|---|
DelegatingConnection |
Connection 实现类,是如下几个类的父类 |
PoolingConnection |
用于包装原生的Connection ,支持缓存prepareStatement 和prepareCall |
PoolableConnection |
用于包装原生的PoolingConnection (若是没有开启poolPreparedStatements ,则包装的只是原生Connection ),调用close() 时只是将链接还给链接池 |
PoolableManagedConnection |
PoolableConnection 的子类,用于包装ManagedConnection ,支持JTA 和XA 事务 |
ManagedConnection |
用于包装原生的Connection ,支持JTA 和XA 事务 |
PoolGuardConnectionWrapper |
用于包装PoolableConnection ,当accessToUnderlyingConnectionAllowed 才能获取底层链接对象。咱们获取到的就是这个对象 |
另外,这里先归纳下得到链接的整个过程:
removeAbandonedOnBorrow
,达到条件会进行检测;DriverConnectionFactory
建立原生对象,再经过PoolableConnectionFactory
包装为池化对象);testOnBorrow
或者testOnCreate
,会经过工厂校验链接有效性;PoolGuardConnectionWrapper
包装链接对象,并返回给客户端前面已经说过,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
是一个很简练的类,里面涉及到的属性设置和锁机制都涉及得很是巧妙。
// 存放着链接池全部的链接对象(但不包含已经释放的) private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>(); // 存放着空闲链接对象的阻塞队列 private final LinkedBlockingDeque<PooledObject<T>> 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<T> 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(); }
这里在建立链接对象时采用的锁机制很是值得学习,简练且高效。
private PooledObject<T> 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<T> 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<T>) p).setRequireFullStackTrace(ac.getRequireFullStackTrace()); } } // 链接数量+1 createdCount.incrementAndGet(); // 将建立的对象放入allObjects allObjects.put(new IdentityWrapper<>(p.getObject()), p); return p; }
public PooledObject<PoolableConnection> 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<DelegatingPreparedStatement> 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<PStmtKey, DelegatingPreparedStatement> 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
线程,因此咱们从BasicDataSource.startPoolMaintenance()
开始分析。
前面说过timeBetweenEvictionRunsMillis
为非正数时不会开启开启空闲对象回收器,从如下代码能够理解具体逻辑。
protected void startPoolMaintenance() { // 只有timeBetweenEvictionRunsMillis为正数,才会开启空闲对象回收器 if (connectionPool != null && timeBetweenEvictionRunsMillis > 0) { connectionPool.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); } }
这个BaseGenericObjectPool
是上面说到的GenericObjectPool
的父类。
public final void setTimeBetweenEvictionRunsMillis( final long timeBetweenEvictionRunsMillis) { // 设置回收线程运行间隔时间 this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; // 继续调用本类的方法,下面继续进入方法分析 startEvictor(timeBetweenEvictionRunsMillis); }
这里会去定义一个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); } } }
DBCP
是使用ScheduledThreadPoolExecutor
来实现回收器的定时检测。 涉及到ThreadPoolExecutor
为JDK
自带的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); }
Evictor
是BaseGenericObjectPool
的内部类,实现了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); } }
这里的回收过程包括如下四道校验:
按照evictionPolicy
校验idleSoftEvictTime
、idleEvictTime
;
利用工厂从新初始化样本,这里会校验maxConnLifetimeMillis
(testWhileIdle
为true);
校验maxConnLifetimeMillis
和validationQueryTimeout
(testWhileIdle
为true);
校验全部链接的未使用时间是否超过removeAbandonedTimeout
(removeAbandonedOnMaintenance
为true)。
public void evict() throws Exception { // 校验当前链接池是否关闭 assertOpen(); if (idleObjects.size() > 0) { PooledObject<T> underTest = null; // 介绍参数时已经讲到,这个evictionPolicy咱们能够自定义 final EvictionPolicy<T> 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
获取PerUserPoolDataSource
和SharedPoolDataSource
对象,选择使用tomcat 9.0.21
做容器。
若是以前没有接触过JNDI
,并不会影响下面例子的理解,其实能够理解为像spring
的bean
配置和获取。
源码分析时已经讲到,除了咱们熟知的BasicDataSource
,DBCP
还提供了经过JDNI
获取数据源,以下表。
类名 | 描述 |
---|---|
InstanceKeyDataSource |
用于支持JDNI 环境的数据源,是如下两个类的父类 |
PerUserPoolDataSource |
InstanceKeyDataSource 的子类,针对每一个用户会单独分配一个链接池,每一个链接池能够设置不一样属性。例如如下需求,相比user,admin 能够建立更多地链接以保证 |
SharedPoolDataSource |
InstanceKeyDataSource 的子类,不一样用户共享一个链接池 |
本文在前面例子的基础上增长如下依赖,由于是web项目,因此打包方式为war
:
<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.2.1</version> <scope>provided</scope> </dependency>
在webapp
文件下建立目录META-INF
,并建立context.xml
文件。这里面的每一个resource
节点都是咱们配置的对象,相似于spring
的bean
节点。其中bean/DriverAdapterCPDS
这个对象须要被另外两个使用到。
<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="bean/SharedPoolDataSourceFactory" auth="Container" type="org.apache.commons.dbcp2.datasources.SharedPoolDataSource" factory="org.apache.commons.dbcp2.datasources.SharedPoolDataSourceFactory" singleton="false" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" username="root" password="root" maxTotal="8" maxIdle="10" dataSourceName="java:comp/env/bean/DriverAdapterCPDS" /> <Resource name="bean/PerUserPoolDataSourceFactory" auth="Container" type="org.apache.commons.dbcp2.datasources.PerUserPoolDataSource" factory="org.apache.commons.dbcp2.datasources.PerUserPoolDataSourceFactory" singleton="false" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" username="root" password="root" maxTotal="8" maxIdle="10" dataSourceName="java:comp/env/bean/DriverAdapterCPDS" /> <Resource name="bean/DriverAdapterCPDS" auth="Container" type="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS" factory="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS" singleton="false" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true" userName="root" userPassword="root" maxIdle="10" /> </Context>
在web-app
节点下配置资源引用,每一个resource-env-ref
指向了咱们配置好的对象。
<resource-env-ref> <description>Test DriverAdapterCPDS</description> <resource-env-ref-name>bean/DriverAdapterCPDS</resource-env-ref-name> <resource-env-ref-type>org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS</resource-env-ref-type> </resource-env-ref> <resource-env-ref> <description>Test SharedPoolDataSource</description> <resource-env-ref-name>bean/SharedPoolDataSourceFactory</resource-env-ref-name> <resource-env-ref-type>org.apache.commons.dbcp2.datasources.SharedPoolDataSource</resource-env-ref-type> </resource-env-ref> <resource-env-ref> <description>Test erUserPoolDataSource</description> <resource-env-ref-name>bean/erUserPoolDataSourceFactory</resource-env-ref-name> <resource-env-ref-type>org.apache.commons.dbcp2.datasources.erUserPoolDataSource</resource-env-ref-type> </resource-env-ref>
由于须要在web
环境中使用,若是直接建类写个main
方法测试,会一直报错的,目前没找到好的办法。这里就简单地使用jsp
来测试吧(这是从tomcat官网参照的例子)。
<body> <% // 得到名称服务的上下文对象 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(); %> </body>
打包项目在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/J conn2=128868782, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=zzf@localhost, MySQL Connector/J
前面源码分析已经讲到,如下类用于支持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
数据库,简单地模拟两个数据库。
测试以前,这里简单回顾下直接使用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
的实现。
<!-- jta:用于测试DBCP对JTA事务的支持 --> <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jdbc</artifactId> <version>3.9.3</version> </dependency>
这里千万记得要设置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