简介

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)。

引入依赖

  1. <!-- junit -->
  2. <dependency>
  3. <groupId>junit</groupId>
  4. <artifactId>junit</artifactId>
  5. <version>4.12</version>
  6. <scope>test</scope>
  7. </dependency>
  8. <!-- dbcp -->
  9. <dependency>
  10. <groupId>org.apache.commons</groupId>
  11. <artifactId>commons-dbcp2</artifactId>
  12. <version>2.6.0</version>
  13. </dependency>
  14. <!-- log4j -->
  15. <dependency>
  16. <groupId>log4j</groupId>
  17. <artifactId>log4j</artifactId>
  18. <version>1.2.17</version>
  19. </dependency>
  20. <!-- mysql驱动的jar包 -->
  21. <dependency>
  22. <groupId>mysql</groupId>
  23. <artifactId>mysql-connector-java</artifactId>
  24. <version>8.0.15</version>
  25. </dependency>

编写dbcp.prperties

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

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

获取连接池和获取连接

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

路径:cn.zzs.dbcp

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

编写测试类

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

  1. @Test
  2. public void save() throws SQLException {
  3. // 创建sql
  4. String sql = "insert into demo_user values(null,?,?,?,?,?)";
  5. Connection connection = null;
  6. PreparedStatement statement = null;
  7. try {
  8. // 获得连接
  9. connection = JDBCUtils.getConnection();
  10. // 开启事务设置非自动提交
  11. connection.setAutoCommit(false);
  12. // 获得Statement对象
  13. statement = connection.prepareStatement(sql);
  14. // 设置参数
  15. statement.setString(1, "zzf003");
  16. statement.setInt(2, 18);
  17. statement.setDate(3, new Date(System.currentTimeMillis()));
  18. statement.setDate(4, new Date(System.currentTimeMillis()));
  19. statement.setBoolean(5, false);
  20. // 执行
  21. statement.executeUpdate();
  22. // 提交事务
  23. connection.commit();
  24. } finally {
  25. // 释放资源
  26. JDBCUtils.release(connection, statement, null);
  27. }
  28. }

配置文件详解

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

基本连接属性

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

  1. driverClassName=com.mysql.cj.jdbc.Driver
  2. url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
  3. username=root
  4. password=root

连接池大小参数

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

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

缓存语句

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

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

连接检查参数

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

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

事务相关参数

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

  1. #-------------事务相关的属性--------------------------------
  2. #连接池创建的连接的默认的auto-commit状态
  3. #默认为空,由驱动决定
  4. defaultAutoCommit=true
  5. #连接池创建的连接的默认的read-only状态。
  6. #默认值为空,由驱动决定
  7. defaultReadOnly=false
  8. #连接池创建的连接的默认的TransactionIsolation状态
  9. #可用值为下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
  10. #默认值为空,由驱动决定
  11. defaultTransactionIsolation=REPEATABLE_READ
  12. #归还连接时是否设置自动提交为true
  13. #默认true
  14. autoCommitOnReturn=true
  15. #归还连接时是否设置回滚事务
  16. #默认true
  17. rollbackOnReturn=true

连接泄漏回收参数

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

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

其他

这部分参数比较少用。

  1. #-------------其他--------------------------------
  2. #是否使用快速失败机制
  3. #默认为空,由驱动决定
  4. fastFailValidation=false
  5. #当使用快速失败机制时,设置触发的异常码
  6. #多个code用","隔开
  7. #disconnectionSqlCodes
  8. #borrow连接的顺序
  9. #默认true
  10. lifo=true
  11. #每个连接创建时执行的语句
  12. #connectionInitSqls=
  13. #连接参数:例如username、password、characterEncoding等都可以在这里设置
  14. #多个参数用";"隔开
  15. #connectionProperties=
  16. #指定数据源的jmx名。注意,配置了才能注册MBean
  17. jmxName=cn.zzs.jmx:type=BasicDataSource,name=zzs001
  18. #查询超时时间
  19. #默认为空,即根据驱动设置
  20. #defaultQueryTimeout=
  21. #控制PoolGuard是否容许获取底层连接
  22. #默认为false
  23. accessToUnderlyingConnectionAllowed=false
  24. #如果容许则可以使用下面的方式来获取底层物理连接:
  25. # Connection conn = ds.getConnection();
  26. # Connection dconn = ((DelegatingConnection) conn).getInnermostDelegate();
  27. # ...
  28. # conn.close();

源码分析

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

创建数据源和连接池

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

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

类名 描述
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()的时候,如下:

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

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

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

BasicDataSource.createDataSource()

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

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

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

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

获取连接对象

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

类名 描述
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()开始研究。

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

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

GenericObjectPool.borrowObject()

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

  1. // 存放着连接池所有的连接对象(但不包含已经释放的)
  2. private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects =
  3. new ConcurrentHashMap<>();
  4. // 存放着空闲连接对象的阻塞队列
  5. private final LinkedBlockingDeque<PooledObject<T>> idleObjects;
  6. // 为n>1表示当前有n个线程正在创建新连接对象
  7. private long makeObjectCount = 0;
  8. // 创建连接对象时所用的锁
  9. private final Object makeObjectCountLock = new Object();
  10. // 连接对象创建总数量
  11. private final AtomicLong createCount = new AtomicLong(0);
  12. public T borrowObject() throws Exception {
  13. // 如果我们设置了连接获取等待时间,“借出”过程就必须在指定时间内完成
  14. return borrowObject(getMaxWaitMillis());
  15. }
  16. public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
  17. // 校验连接池是否打开状态
  18. assertOpen();
  19. // 如果设置了removeAbandonedOnBorrow,达到触发条件是会遍历所有连接,未使用时长超过removeAbandonedTimeout的将被释放掉(一般可以检测出泄露连接)
  20. final AbandonedConfig ac = this.abandonedConfig;
  21. if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
  22. (getNumIdle() < 2) &&
  23. (getNumActive() > getMaxTotal() - 3) ) {
  24. removeAbandoned(ac);
  25. }
  26. PooledObject<T> p = null;
  27. // 连接数达到maxTotal是否阻塞等待
  28. final boolean blockWhenExhausted = getBlockWhenExhausted();
  29. boolean create;
  30. final long waitTime = System.currentTimeMillis();
  31. // 如果获取的连接对象为空,会再次进入获取
  32. while (p == null) {
  33. create = false;
  34. // 获取空闲队列的第一个元素,如果为空就试图创建新连接
  35. p = idleObjects.pollFirst();
  36. if (p == null) {
  37. // 后面分析这个方法
  38. p = create();
  39. if (p != null) {
  40. create = true;
  41. }
  42. }
  43. // 连接数达到maxTotal且暂时没有空闲连接,这时需要阻塞等待,直到获得空闲队列中的连接或等待超时
  44. if (blockWhenExhausted) {
  45. if (p == null) {
  46. if (borrowMaxWaitMillis < 0) {
  47. // 无限等待
  48. p = idleObjects.takeFirst();
  49. } else {
  50. // 等待maxWaitMillis
  51. p = idleObjects.pollFirst(borrowMaxWaitMillis,
  52. TimeUnit.MILLISECONDS);
  53. }
  54. }
  55. // 这个时候还是没有就只能抛出异常
  56. if (p == null) {
  57. throw new NoSuchElementException(
  58. "Timeout waiting for idle object");
  59. }
  60. } else {
  61. if (p == null) {
  62. throw new NoSuchElementException("Pool exhausted");
  63. }
  64. }
  65. // 如果连接处于空闲状态,会修改连接的state、lastBorrowTime、lastUseTime、borrowedCount等,并返回true
  66. if (!p.allocate()) {
  67. p = null;
  68. }
  69. if (p != null) {
  70. // 利用工厂重新初始化连接对象,这里会去校验连接存活时间、设置lastUsedTime、及其他初始参数
  71. try {
  72. factory.activateObject(p);
  73. } catch (final Exception e) {
  74. try {
  75. destroy(p);
  76. } catch (final Exception e1) {
  77. // Ignore - activation failure is more important
  78. }
  79. p = null;
  80. if (create) {
  81. final NoSuchElementException nsee = new NoSuchElementException(
  82. "Unable to activate object");
  83. nsee.initCause(e);
  84. throw nsee;
  85. }
  86. }
  87. // 根据设置的参数,判断是否检测连接有效性
  88. if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
  89. boolean validate = false;
  90. Throwable validationThrowable = null;
  91. try {
  92. // 这里会去校验连接的存活时间是否超过maxConnLifetimeMillis,以及通过SQL去校验执行时间
  93. validate = factory.validateObject(p);
  94. } catch (final Throwable t) {
  95. PoolUtils.checkRethrow(t);
  96. validationThrowable = t;
  97. }
  98. // 如果校验不通过,会释放该对象
  99. if (!validate) {
  100. try {
  101. destroy(p);
  102. destroyedByBorrowValidationCount.incrementAndGet();
  103. } catch (final Exception e) {
  104. // Ignore - validation failure is more important
  105. }
  106. p = null;
  107. if (create) {
  108. final NoSuchElementException nsee = new NoSuchElementException(
  109. "Unable to validate object");
  110. nsee.initCause(validationThrowable);
  111. throw nsee;
  112. }
  113. }
  114. }
  115. }
  116. }
  117. // 更新borrowedCount、idleTimes和waitTimes
  118. updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
  119. return p.getObject();
  120. }

GenericObjectPool.create()

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

  1. private PooledObject<T> create() throws Exception {
  2. int localMaxTotal = getMaxTotal();
  3. if (localMaxTotal < 0) {
  4. localMaxTotal = Integer.MAX_VALUE;
  5. }
  6. final long localStartTimeMillis = System.currentTimeMillis();
  7. final long localMaxWaitTimeMillis = Math.max(getMaxWaitMillis(), 0);
  8. // 创建标识:
  9. // - TRUE: 调用工厂创建返回对象
  10. // - FALSE: 直接返回null
  11. // - null: 继续循环
  12. Boolean create = null;
  13. while (create == null) {
  14. synchronized (makeObjectCountLock) {
  15. final long newCreateCount = createCount.incrementAndGet();
  16. if (newCreateCount > localMaxTotal) {
  17. // 当前池已经达到maxTotal,或者有另外一个线程正在试图创建一个新的连接使之达到容量极限
  18. createCount.decrementAndGet();
  19. if (makeObjectCount == 0) {
  20. // 连接池确实已达到容量极限
  21. create = Boolean.FALSE;
  22. } else {
  23. // 当前另外一个线程正在试图创建一个新的连接使之达到容量极限,此时需要等待
  24. makeObjectCountLock.wait(localMaxWaitTimeMillis);
  25. }
  26. } else {
  27. // 当前连接池容量未到达极限,可以继续创建连接对象
  28. makeObjectCount++;
  29. create = Boolean.TRUE;
  30. }
  31. }
  32. // 当达到maxWaitTimeMillis时不创建连接对象,直接退出循环
  33. if (create == null &&
  34. (localMaxWaitTimeMillis > 0 &&
  35. System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) {
  36. create = Boolean.FALSE;
  37. }
  38. }
  39. if (!create.booleanValue()) {
  40. return null;
  41. }
  42. final PooledObject<T> p;
  43. try {
  44. // 调用工厂创建对象,后面对这个方法展开分析
  45. p = factory.makeObject();
  46. } catch (final Throwable e) {
  47. createCount.decrementAndGet();
  48. throw e;
  49. } finally {
  50. synchronized (makeObjectCountLock) {
  51. // 创建标识-1
  52. makeObjectCount--;
  53. // 唤醒makeObjectCountLock锁住的对象
  54. makeObjectCountLock.notifyAll();
  55. }
  56. }
  57. final AbandonedConfig ac = this.abandonedConfig;
  58. if (ac != null && ac.getLogAbandoned()) {
  59. p.setLogAbandoned(true);
  60. // TODO: in 3.0, this can use the method defined on PooledObject
  61. if (p instanceof DefaultPooledObject<?>) {
  62. ((DefaultPooledObject<T>) p).setRequireFullStackTrace(ac.getRequireFullStackTrace());
  63. }
  64. }
  65. // 连接数量+1
  66. createdCount.incrementAndGet();
  67. // 将创建的对象放入allObjects
  68. allObjects.put(new IdentityWrapper<>(p.getObject()), p);
  69. return p;
  70. }

PoolableConnectionFactory.makeObject()

  1. public PooledObject<PoolableConnection> makeObject() throws Exception {
  2. // 创建原生的Connection对象
  3. Connection conn = connectionFactory.createConnection();
  4. if (conn == null) {
  5. throw new IllegalStateException("Connection factory returned null from createConnection");
  6. }
  7. try {
  8. // 执行我们设置的connectionInitSqls
  9. initializeConnection(conn);
  10. } catch (final SQLException sqle) {
  11. // Make sure the connection is closed
  12. try {
  13. conn.close();
  14. } catch (final SQLException ignore) {
  15. // ignore
  16. }
  17. // Rethrow original exception so it is visible to caller
  18. throw sqle;
  19. }
  20. // 连接索引+1
  21. final long connIndex = connectionIndex.getAndIncrement();
  22. // 如果设置了poolPreparedStatements,则创建包装连接为PoolingConnection对象
  23. if (poolStatements) {
  24. conn = new PoolingConnection(conn);
  25. final GenericKeyedObjectPoolConfig<DelegatingPreparedStatement> config = new GenericKeyedObjectPoolConfig<>();
  26. config.setMaxTotalPerKey(-1);
  27. config.setBlockWhenExhausted(false);
  28. config.setMaxWaitMillis(0);
  29. config.setMaxIdlePerKey(1);
  30. config.setMaxTotal(maxOpenPreparedStatements);
  31. if (dataSourceJmxObjectName != null) {
  32. final StringBuilder base = new StringBuilder(dataSourceJmxObjectName.toString());
  33. base.append(Constants.JMX_CONNECTION_BASE_EXT);
  34. base.append(Long.toString(connIndex));
  35. config.setJmxNameBase(base.toString());
  36. config.setJmxNamePrefix(Constants.JMX_STATEMENT_POOL_PREFIX);
  37. } else {
  38. config.setJmxEnabled(false);
  39. }
  40. final PoolingConnection poolingConn = (PoolingConnection) conn;
  41. final KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> stmtPool = new GenericKeyedObjectPool<>(
  42. poolingConn, config);
  43. poolingConn.setStatementPool(stmtPool);
  44. poolingConn.setCacheState(cacheState);
  45. }
  46. // 用于注册连接到JMX
  47. ObjectName connJmxName;
  48. if (dataSourceJmxObjectName == null) {
  49. connJmxName = null;
  50. } else {
  51. connJmxName = new ObjectName(
  52. dataSourceJmxObjectName.toString() + Constants.JMX_CONNECTION_BASE_EXT + connIndex);
  53. }
  54. // 创建PoolableConnection对象
  55. final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, disconnectionSqlCodes,
  56. fastFailValidation);
  57. pc.setCacheState(cacheState);
  58. // 包装成连接池所需的对象
  59. return new DefaultPooledObject<>(pc);
  60. }

空闲对象回收器Evictor

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

BasicDataSource.startPoolMaintenance()

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

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

BaseGenericObjectPool.setTimeBetweenEvictionRunsMillis(long)

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

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

BaseGenericObjectPool.startEvictor(long)

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

  1. final void startEvictor(final long delay) {
  2. synchronized (evictionLock) {
  3. if (null != evictor) {
  4. EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
  5. evictor = null;
  6. evictionIterator = null;
  7. }
  8. // 创建回收器任务,并执行定时调度
  9. if (delay > 0) {
  10. evictor = new Evictor();
  11. EvictionTimer.schedule(evictor, delay, delay);
  12. }
  13. }
  14. }

EvictionTimer.schedule(Evictor, long, long)

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

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

BaseGenericObjectPool.Evictor

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

  1. class Evictor implements Runnable {
  2. private ScheduledFuture<?> scheduledFuture;
  3. @Override
  4. public void run() {
  5. final ClassLoader savedClassLoader =
  6. Thread.currentThread().getContextClassLoader();
  7. try {
  8. // 确保回收器使用的类加载器和工厂对象的一样
  9. if (factoryClassLoader != null) {
  10. final ClassLoader cl = factoryClassLoader.get();
  11. if (cl == null) {
  12. cancel();
  13. return;
  14. }
  15. Thread.currentThread().setContextClassLoader(cl);
  16. }
  17. try {
  18. // 回收符合条件的对象,后面继续扩展
  19. evict();
  20. } catch(final Exception e) {
  21. swallowException(e);
  22. } catch(final OutOfMemoryError oome) {
  23. // Log problem but give evictor thread a chance to continue
  24. // in case error is recoverable
  25. oome.printStackTrace(System.err);
  26. }
  27. try {
  28. // 确保最小空闲对象
  29. ensureMinIdle();
  30. } catch (final Exception e) {
  31. swallowException(e);
  32. }
  33. } finally {
  34. Thread.currentThread().setContextClassLoader(savedClassLoader);
  35. }
  36. }
  37. void setScheduledFuture(final ScheduledFuture<?> scheduledFuture) {
  38. this.scheduledFuture = scheduledFuture;
  39. }
  40. void cancel() {
  41. scheduledFuture.cancel(false);
  42. }
  43. }

GenericObjectPool.evict()

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

  1. 按照evictionPolicy校验idleSoftEvictTimeidleEvictTime

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

  3. 校验maxConnLifetimeMillisvalidationQueryTimeouttestWhileIdle为true);

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

  1. public void evict() throws Exception {
  2. // 校验当前连接池是否关闭
  3. assertOpen();
  4. if (idleObjects.size() > 0) {
  5. PooledObject<T> underTest = null;
  6. // 介绍参数时已经讲到,这个evictionPolicy我们可以自定义
  7. final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();
  8. synchronized (evictionLock) {
  9. final EvictionConfig evictionConfig = new EvictionConfig(
  10. getMinEvictableIdleTimeMillis(),
  11. getSoftMinEvictableIdleTimeMillis(),
  12. getMinIdle());
  13. final boolean testWhileIdle = getTestWhileIdle();
  14. // 获取我们指定的样本数,并开始遍历
  15. for (int i = 0, m = getNumTests(); i < m; i++) {
  16. if (evictionIterator == null || !evictionIterator.hasNext()) {
  17. evictionIterator = new EvictionIterator(idleObjects);
  18. }
  19. if (!evictionIterator.hasNext()) {
  20. // Pool exhausted, nothing to do here
  21. return;
  22. }
  23. try {
  24. underTest = evictionIterator.next();
  25. } catch (final NoSuchElementException nsee) {
  26. // 当前样本正被另一个线程借出
  27. i--;
  28. evictionIterator = null;
  29. continue;
  30. }
  31. // 判断如果样本是空闲状态,设置为EVICTION状态
  32. // 如果不是,说明另一个线程已经借出了这个样本
  33. if (!underTest.startEvictionTest()) {
  34. i--;
  35. continue;
  36. }
  37. boolean evict;
  38. try {
  39. // 调用回收策略来判断是否回收该样本,按照默认策略,以下情况都会返回true:
  40. // 1. 样本空闲时间大于我们设置的idleSoftEvictTime,且当前池中空闲连接数量>minIdle
  41. // 2. 样本空闲时间大于我们设置的idleEvictTime
  42. evict = evictionPolicy.evict(evictionConfig, underTest,
  43. idleObjects.size());
  44. } catch (final Throwable t) {
  45. PoolUtils.checkRethrow(t);
  46. swallowException(new Exception(t));
  47. evict = false;
  48. }
  49. // 如果需要回收,则释放这个样本
  50. if (evict) {
  51. destroy(underTest);
  52. destroyedByEvictorCount.incrementAndGet();
  53. } else {
  54. // 如果设置了testWhileIdle,会
  55. if (testWhileIdle) {
  56. boolean active = false;
  57. try {
  58. // 利用工厂重新初始化样本,这里会校验maxConnLifetimeMillis
  59. factory.activateObject(underTest);
  60. active = true;
  61. } catch (final Exception e) {
  62. // 抛出异常标识校验不通过,释放样本
  63. destroy(underTest);
  64. destroyedByEvictorCount.incrementAndGet();
  65. }
  66. if (active) {
  67. // 接下来会校验maxConnLifetimeMillis和validationQueryTimeout
  68. if (!factory.validateObject(underTest)) {
  69. destroy(underTest);
  70. destroyedByEvictorCount.incrementAndGet();
  71. } else {
  72. try {
  73. // 这里会将样本rollbackOnReturn、autoCommitOnReturn等
  74. factory.passivateObject(underTest);
  75. } catch (final Exception e) {
  76. destroy(underTest);
  77. destroyedByEvictorCount.incrementAndGet();
  78. }
  79. }
  80. }
  81. }
  82. // 如果状态为EVICTION或EVICTION_RETURN_TO_HEAD,修改为IDLE
  83. if (!underTest.endEvictionTest(idleObjects)) {
  84. //空
  85. }
  86. }
  87. }
  88. }
  89. }
  90. // 校验所有连接的未使用时间是否超过removeAbandonedTimeout
  91. final AbandonedConfig ac = this.abandonedConfig;
  92. if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
  93. removeAbandoned(ac);
  94. }
  95. }

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

通过JNDI获取数据源对象

需求

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

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

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

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

引入依赖

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

  1. <dependency>
  2. <groupId>javax.servlet</groupId>
  3. <artifactId>jstl</artifactId>
  4. <version>1.2</version>
  5. <scope>provided</scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>javax.servlet</groupId>
  9. <artifactId>javax.servlet-api</artifactId>
  10. <version>3.1.0</version>
  11. <scope>provided</scope>
  12. </dependency>
  13. <dependency>
  14. <groupId>javax.servlet.jsp</groupId>
  15. <artifactId>javax.servlet.jsp-api</artifactId>
  16. <version>2.2.1</version>
  17. <scope>provided</scope>
  18. </dependency>

编写context.xml

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

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <Context>
  3. <Resource
  4. name="bean/SharedPoolDataSourceFactory"
  5. auth="Container"
  6. type="org.apache.commons.dbcp2.datasources.SharedPoolDataSource"
  7. factory="org.apache.commons.dbcp2.datasources.SharedPoolDataSourceFactory"
  8. singleton="false"
  9. driverClassName="com.mysql.cj.jdbc.Driver"
  10. url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true"
  11. username="root"
  12. password="root"
  13. maxTotal="8"
  14. maxIdle="10"
  15. dataSourceName="java:comp/env/bean/DriverAdapterCPDS"
  16. />
  17. <Resource
  18. name="bean/PerUserPoolDataSourceFactory"
  19. auth="Container"
  20. type="org.apache.commons.dbcp2.datasources.PerUserPoolDataSource"
  21. factory="org.apache.commons.dbcp2.datasources.PerUserPoolDataSourceFactory"
  22. singleton="false"
  23. driverClassName="com.mysql.cj.jdbc.Driver"
  24. url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true"
  25. username="root"
  26. password="root"
  27. maxTotal="8"
  28. maxIdle="10"
  29. dataSourceName="java:comp/env/bean/DriverAdapterCPDS"
  30. />
  31. <Resource
  32. name="bean/DriverAdapterCPDS"
  33. auth="Container"
  34. type="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS"
  35. factory="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS"
  36. singleton="false"
  37. driverClassName="com.mysql.cj.jdbc.Driver"
  38. url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true"
  39. userName="root"
  40. userPassword="root"
  41. maxIdle="10"
  42. />
  43. </Context>

编写web.xml

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

  1. <resource-env-ref>
  2. <description>Test DriverAdapterCPDS</description>
  3. <resource-env-ref-name>bean/DriverAdapterCPDS</resource-env-ref-name>
  4. <resource-env-ref-type>org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS</resource-env-ref-type>
  5. </resource-env-ref>
  6. <resource-env-ref>
  7. <description>Test SharedPoolDataSource</description>
  8. <resource-env-ref-name>bean/SharedPoolDataSourceFactory</resource-env-ref-name>
  9. <resource-env-ref-type>org.apache.commons.dbcp2.datasources.SharedPoolDataSource</resource-env-ref-type>
  10. </resource-env-ref>
  11. <resource-env-ref>
  12. <description>Test erUserPoolDataSource</description>
  13. <resource-env-ref-name>bean/erUserPoolDataSourceFactory</resource-env-ref-name>
  14. <resource-env-ref-type>org.apache.commons.dbcp2.datasources.erUserPoolDataSource</resource-env-ref-type>
  15. </resource-env-ref>

编写jsp

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

  1. <body>
  2. <%
  3. // 获得名称服务的上下文对象
  4. Context initCtx = new InitialContext();
  5. Context envCtx = (Context)initCtx.lookup("java:comp/env/");
  6. // 查找指定名字的对象
  7. DataSource ds = (DataSource)envCtx.lookup("bean/SharedPoolDataSourceFactory");
  8. DataSource ds2 = (DataSource)envCtx.lookup("bean/PerUserPoolDataSourceFactory");
  9. // 获取连接
  10. Connection conn = ds.getConnection("root","root");
  11. System.out.println("conn" + conn);
  12. Connection conn2 = ds2.getConnection("zzf","zzf");
  13. System.out.println("conn2" + conn2);
  14. // ... 使用连接操作数据库,以及释放资源 ...
  15. conn.close();
  16. conn2.close();
  17. %>
  18. </body>

测试结果

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

  1. conn=1971654708, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=root@localhost, MySQL Connector/J
  2. conn2=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事务,其他线程进来即使只读也是不行的)。

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

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

mysql的XA事务使用

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

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

引入依赖

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

  1. <!-- jta:用于测试DBCP对JTA事务的支持 -->
  2. <dependency>
  3. <groupId>javax.transaction</groupId>
  4. <artifactId>jta</artifactId>
  5. <version>1.1</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>com.atomikos</groupId>
  9. <artifactId>transactions-jdbc</artifactId>
  10. <version>3.9.3</version>
  11. </dependency>

获取BasicManagedDataSource

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

  1. public BasicManagedDataSource getBasicManagedDataSource(
  2. TransactionManager transactionManager,
  3. String url,
  4. String username,
  5. String password) {
  6. BasicManagedDataSource basicManagedDataSource = new BasicManagedDataSource();
  7. basicManagedDataSource.setTransactionManager(transactionManager);
  8. basicManagedDataSource.setUrl(url);
  9. basicManagedDataSource.setUsername(username);
  10. basicManagedDataSource.setPassword(password);
  11. basicManagedDataSource.setDefaultAutoCommit(false);
  12. basicManagedDataSource.setXADataSource("com.mysql.cj.jdbc.MysqlXADataSource");
  13. return basicManagedDataSource;
  14. }
  15. @Test
  16. public void test01() throws Exception {
  17. // 获得事务管理器
  18. TransactionManager transactionManager = new UserTransactionManager();
  19. // 获取第一个数据库的数据源
  20. BasicManagedDataSource basicManagedDataSource1 = getBasicManagedDataSource(
  21. transactionManager,
  22. "jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true",
  23. "root",
  24. "root");
  25. // 注意,这一步非常重要
  26. basicManagedDataSource1.setDefaultCatalog("github_demo");
  27. // 获取第二个数据库的数据源
  28. BasicManagedDataSource basicManagedDataSource2 = getBasicManagedDataSource(
  29. transactionManager,
  30. "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true",
  31. "zzf",
  32. "zzf");
  33. // 注意,这一步非常重要
  34. basicManagedDataSource1.setDefaultCatalog("test");
  35. }

编写两阶段提交的代码

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

  1. @Test
  2. public void test01() throws Exception {
  3. Connection connection1 = null;
  4. Statement statement1 = null;
  5. Connection connection2 = null;
  6. Statement statement2 = null;
  7. transactionManager.begin();
  8. try {
  9. // 获取连接并进行数据库操作,这里会将会将XAResource注册到当前线程的XA事务对象
  10. /**
  11. * XA START xid1;-- 启动一个事务,并使之为active状态
  12. */
  13. connection1 = basicManagedDataSource1.getConnection();
  14. statement1 = connection1.createStatement();
  15. /**
  16. * update github_demo.demo_user set deleted = 1 where id = '1'; -- 事务中的语句
  17. */
  18. boolean result1 = statement1.execute("update github_demo.demo_user set deleted = 1 where id = '1'");
  19. System.out.println(result1);
  20. /**
  21. * XA START xid2;-- 启动一个事务,并使之为active状态
  22. */
  23. connection2 = basicManagedDataSource2.getConnection();
  24. statement2 = connection2.createStatement();
  25. /**
  26. * update test.demo_user set deleted = 1 where id = '1'; -- 事务中的语句
  27. */
  28. boolean result2 = statement2.execute("update test.demo_user set deleted = 1 where id = '1'");
  29. System.out.println(result2);
  30. /**
  31. * 当这执行以下语句:
  32. * XA END xid1; -- 把事务置为idle状态
  33. * XA PREPARE xid1; -- 把事务置为prepare状态
  34. * XA END xid2; -- 把事务置为idle状态
  35. * XA PREPARE xid2; -- 把事务置为prepare状态
  36. * XA COMMIT xid1; -- 提交事务
  37. * XA COMMIT xid2; -- 提交事务
  38. */
  39. transactionManager.commit();
  40. } catch(Exception e) {
  41. e.printStackTrace();
  42. } finally {
  43. statement1.close();
  44. statement2.close();
  45. connection1.close();
  46. connection2.close();
  47. }
  48. }

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

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

DBCP2的使用例子和源码详解(不包括JNDI和JTA支持的使用)的更多相关文章

  1. dom4j的测试例子和源码详解(重点对比和DOM、SAX的区别)

    目录 简介 DOM.SAX.JAXP和DOM4J xerces解释器 SAX DOM JAXP DOM解析器 获取SAX解析器 DOM4j 项目环境 工程环境 创建项目 引入依赖 使用例子--生成xm ...

  2. jdbc-mysql测试例子和源码详解

    目录 简介 什么是JDBC 几个重要的类 使用中的注意事项 使用例子 需求 工程环境 主要步骤 创建表 创建项目 引入依赖 编写jdbc.prperties 获得Connection对象 使用Conn ...

  3. cglib测试例子和源码详解

    目录 简介 为什么会有动态代理? 常见的动态代理有哪些? 什么是cglib 使用例子 需求 工程环境 主要步骤 创建项目 引入依赖 编写被代理类 编写MethodInterceptor接口实现类 编写 ...

  4. [Spark内核] 第40课:CacheManager彻底解密:CacheManager运行原理流程图和源码详解

    本课主题 CacheManager 运行原理图 CacheManager 源码解析 CacheManager 运行原理图 [下图是CacheManager的运行原理图] 首先 RDD 是通过 iter ...

  5. [Qt Creator 快速入门] 第2章 Qt程序编译和源码详解

    一.编写 Hello World Gui程序 Hello World程序就是让应用程序显示"Hello World"字符串.这是最简单的应用,但却包含了一个应用程序的基本要素,所以 ...

  6. Spark Sort-Based Shuffle具体实现内幕和源码详解

    为什么讲解Sorted-Based shuffle?2方面的原因:一,可能有些朋友看到Sorted-Based Shuffle的时候,会有一个误解,认为Spark基于Sorted-Based Shuf ...

  7. Arouter核心思路和源码详解

    前言 阅读本文之前,建议读者: 对Arouter的使用有一定的了解. 对Apt技术有所了解. Arouter是一款Alibaba出品的优秀的路由框架,本文不对其进行全面的分析,只对其最重要的功能进行源 ...

  8. go map数据结构和源码详解

    目录 1. 前言 2. go map的数据结构 2.1 核心结体体 2.2 数据结构图 3. go map的常用操作 3.1 创建 3.2 插入或更新 3.3 删除 3.4 查找 3.5 range迭 ...

  9. 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码

    简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...

随机推荐

  1. MySQL的sleep函数的特殊特现象

      MySQL中的系统函数sleep,实际应用的场景不多,一般用来做实验测试,昨天在测试的时候,意外发现sleep函数的一个特殊现象.如果在查询语句中使用sleep函数,那么休眠的时间跟返回的记录有关 ...

  2. 学习笔记52_mongodb增删改查

    使用use db1作为当前数据库 如果没有db1,会自动创建 使用switch db2,当前数据库切换为db2 使用show dbs,显示当前所有数据库 使用show collection ,显示当前 ...

  3. [BZOJ4553][HEOI2016/TJOI2016]序列

    传送门 好像是DP再套个裸的CDQ? 树套树是不可能写树套树的,这辈子都不可能写树套树的 对于一个 \(i\) ,设它最小为 \(a_i\) ,原数为 \(b_i\) ,最大为 \(c_i\) \(f ...

  4. [考试反思]1015csp-s模拟测试75:混乱

    赶上一套极其傻逼的题(是傻逼,不是简单) T1超级卡精 T2模拟(输出卡"0.0"与"-0.0"不开spj),而且数据诡异乱打就能A(貌似给这道题的时间越长分越 ...

  5. UWP 带左右滚动按钮的横向ListView———仿NetFlix首页河的设计

    也是之前写的控件了,模仿NetFlix的河的设计. 大体要求如下: 1. 横向的ListView 2. 左右按钮,可以左右移动河卡片,左右的滚动条不可见 3. 左右按钮仅在鼠标Hover事件中可见 大 ...

  6. 掌握git命令的正确使用姿势

    前言 最近在团队内部发起了一个小的python项目(用tkinter实现一个小工具),但是发现大家对git的使用还不太熟悉,不知道怎么同步代码.解决冲突等等.因为我觉得对测试工程师来说,git应该是必 ...

  7. 数据结构--树链剖分准备之LCA

    有关LCA的模板题    传送门 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询问的个数和 ...

  8. vue 单页面应用 app自适应方案

    本文是使用淘宝的方案进行布局开发的,遇到的问题是会对app内使用的第三方插件,当页面进行缩放后,比如高德地图中的文字会显得过小,我使用的方法就是手动的动每一个尺寸进行手动的px 到 rem的替换,而不 ...

  9. Flink入门(一)——Apache Flink介绍

    Apache Flink是什么? ​ 在当代数据量激增的时代,各种业务场景都有大量的业务数据产生,对于这些不断产生的数据应该如何进行有效的处理,成为当下大多数公司所面临的问题.随着雅虎对hadoop的 ...

  10. Matlab 文件格式化/Matlab Source File Formator

    由于需要使用到别人编写的Matlab代码文件,但是呢不同的人有不同的风格,有的写得就比较糟糕了. 为了更好地理解代码的内容,一个比较美观的代码会让人身心愉悦. 但是在网上并没有找到一个比较好的实现,此 ...