天天看点

C3P0数据连接池源码-获取连接对象的过程

作者:老吾频道

源码分析

C3P0是一个功能强大的数据库连接池库,但是它的源码确实令人望而生畏。没有注释和奇特的代码格式使得阅读起来非常困难。因此,当我刚开始接触C3P0时,我并没有深入研究它的源码,因为我觉得这是一项艰巨的任务。

然而,现在我决定再次仔细阅读C3P0的源码,尽管需要花费相当多的时间。在这次源码分析中,我将重点关注类与类之间的关系以及一些重要功能的实现,而不像以前那样一步步地进行探索。

此外,C3P0广泛使用了监听器和多线程技术,它们是Java开发中的常见功能,因此本文不会深入探讨其原理。如果你对这些内容感兴趣,我建议你进一步学习,因为在实际项目中你也可能会用到它们。

通过这次源码分析,我希望能够更好地理解C3P0的内部工作原理,提升对连接池的理解,并且能够更好地使用和调优C3P0来满足实际项目的需求。

创建数据源对象

我们使用c3p0时,一般会以ComboPooledDataSource这个类为入口,那么就从这个类展开吧。首先,看下ComboPooledDataSource的UML图。(图片来源于网络)

C3P0数据连接池源码-获取连接对象的过程

下面重点说下几个类的作用:

类名 描述
DataSource 用于创建原生的Connection
ConnectionPoolDataSource 用于创建PooledConnection
PooledDataSource 用于支持对c3p0连接池中连接数量和状态等的监控
IdentityTokenized 用于支持注册功能。每个DataSource实例都有一个identityToken,用于在C3P0Registry中注册
PoolBackedDataSourceBase 实现了IdentityTokenized接口,还持有PropertyChangeSupport和VetoableChangeSupport对象,并提供了添加和移除监听器的方法
AbstractPoolBackedDataSource 实现了PooledDataSource和DataSource
AbstractComboPooledDataSource 提供了数据源参数配置的setter/getter方法
DriverManagerDataSource DataSource实现类,用于创建原生的Connection
WrapperConnectionPoolDataSource ConnectionPoolDataSource实现类,用于创建PooledConnection
C3P0PooledConnectionPoolManager 连接池管理器,非常重要。用于创建连接池,并持有连接池的Map(根据账号密码匹配连接池)。

new一个ComboPooledDataSource对象时,主要做了几件事:

  1. 获得this的identityToken,并注册到C3P0Registry
  2. 添加监听配置参数改变的Listenner
  3. 创建DriverManagerDataSource和WrapperConnectionPoolDataSource对象

(此处已添加书籍卡片,请到今日头条客户端查看)

获得this的identityToken,并注册到C3P0Registry

在C3P0中,身份令牌是为了区分不同的连接池而引入的。每个连接池都有一个唯一的身份令牌,用于标识该连接池。通过身份令牌,C3P0能够准确地定位和管理连接池。

allocateIdentityToken方法的作用是分配一个新的身份令牌。它确保每个连接池都具有唯一的身份令牌,以避免冲突和混淆。

在该方法的实现中,C3P0使用了一种基于计数器的算法来生成身份令牌。每次调用allocateIdentityToken方法时,计数器会递增,并将递增后的值作为新的身份令牌。这样可以确保每个连接池都有一个不同的身份令牌。

通过分配唯一的身份令牌,C3P0能够正确地管理多个连接池,并确保它们之间的隔离性和准确性。

C3P0数据连接池源码-获取连接对象的过程

allocateIdentityToken

接下来,再来看下注册过程,调用的是C3P0Registry的incorporate方法。

C3P0数据连接池源码-获取连接对象的过程

incorporate

注册数据源的过程相对简单易懂,但有一个奇怪的地方在于,通常在注册过程中,我们会期望有一种方法可以通过唯一标识去查找数据源对象。然而,在C3P0中,即使我们知道了某个数据源的身份令牌(identity token),仍然无法直接获取对应的数据源对象,因为C3P0Registry并没有提供相关的方法。

后来我发现,我们既不能也不应该通过身份令牌来查找数据源,而是应该使用数据源的名称(dataSourceName)进行查找。幸运的是,C3P0Registry提供了这样的方法。因此,为了在程序的任何位置都能够获取到数据源对象,我们应该在创建数据源时就设置好它的数据源名称。

通过使用数据源名称作为唯一标识,我们可以在需要的时候通过C3P0Registry来获取数据源对象。这种方式更加直观和方便,使得我们能够轻松地管理和访问多个数据源。

C3P0数据连接池源码-获取连接对象的过程

pooledDataSourceByName

创建DriverManagerDataSource

C3P0数据连接池源码-获取连接对象的过程

UML

创建DriverManagerDataSource实例主要做了三件事:

1. 获得this的identityToken,并注册到C3P0Registry

2. 添加监听配置参数改变的Listenner(当driverClass属性更改时触发事件)

3. 读取配置文件,初始化默认的user和password

C3P0数据连接池源码-获取连接对象的过程

创建DriverManagerDataSource

创建WrapperConnectionPoolDataSource

C3P0数据连接池源码-获取连接对象的过程

创建WrapperConnectionPoolDataSource,主要做了以下三件件事:

1. 获得this的identityToken,并注册到C3P0Registry

2. 添加监听配置参数改变的Listenner(当connectionTesterClassName属性更改时实例化ConnectionTester,当userOverridesAsString更改时重新解析字符串)

3. 解析userOverridesAsString

C3P0数据连接池源码-获取连接对象的过程

以上基本将ComboPooledDataSource的内容讲完,下面介绍连接池的创建。

创建连接池对象

C3P0数据连接池源码-获取连接对象的过程

创建连接池的过程可以概括为四个步骤:

  1. 创建C3P0PooledConnectionPoolManager对象,开启另一个线程来初始化timer、taskRunner、deferredStatementDestroyer、rpfact和authsToPools等属性
  2. 创建默认账号密码对应的C3P0PooledConnectionPool对象,并创建PooledConnectionResourcePoolManager对象
  3. 创建BasicResourcePool对象,创建initialPoolSize对应的初始连接,开启检查连接是否过期、以及检查空闲连接有效性的定时任务
C3P0数据连接池源码-获取连接对象的过程

当我们完成数据源的创建后,并没有立即创建连接池。实际上,只有在我们调用getConnection方法时,连接池才会被触发创建。这是因为AbstractPoolBackedDataSource类实现了DataSource接口,其中包含了连接池的创建和管理逻辑。

AbstractPoolBackedDataSource作为连接池数据源的抽象实现,提供了一种统一的方式来管理连接池和数据源的关系。它将连接池的创建和初始化延迟到第一次请求连接时,这样可以避免不必要的资源浪费。

在调用getConnection方法时,AbstractPoolBackedDataSource会检查连接池是否已经创建,如果没有,则会触发连接池的创建过程。连接池的创建涉及到多个步骤,包括创建连接池对象、设置连接池的属性和参数、初始化连接数等。这些步骤保证了连接池在被使用之前处于正确的状态。

通过延迟创建连接池,我们能够在真正需要连接时才进行资源的分配和管理,避免了不必要的资源消耗。这种设计思想可以提高系统的性能和效率,同时也使得连接池的使用更加灵活和可控。

C3P0数据连接池源码-获取连接对象的过程

下面介绍下这几个类:

类名 描述
C3P0PooledConnectionPoolManager 连接池管理器。主要用于获取/创建连接池,它持有DbAuth-C3P0PooledConnectionPool键值对的Map
C3P0PooledConnectionPool 连接池。主要用于检入和检出连接对象,实际调用的是其持有的BasicResourcePool对象
BasicResourcePool 资源池。主要用于检入和检出连接对象
PooledConnectionResourcePoolManager 资源管理器。主要用于创建新的连接对象,以及检入、检出或空闲时进行连接测试

创建连接对象

我总结下获取连接的过程,为以下几步:

  1. 从BasicResourcePool的空闲连接中获取,如果没有,会尝试去创建新的连接,当然,创建的过程也是异步的
  2. 开启缓存语句支持
  3. 判断连接是否正在被空闲资源检测线程使用,如果是,重新获取连接
  4. 校验连接是否过期
  5. 检出测试
  6. 判断连接原来的Statement是不是已经清除完,如果没有,重新获取连接
  7. 设置监听器后将连接返回给客户端

C3P0PooledConnectionPool.checkoutPooledConnection()

c3p0使用的是监听器的方式,当客户端调用close()方法时会触发监听器把连接checkin到连接池中。

C3P0数据连接池源码-获取连接对象的过程

C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse()

用于从连接池中获取连接,并标记该连接为正在使用状态。

C3P0数据连接池源码-获取连接对象的过程

BasicResourcePool.checkoutResource(long)

在C3P0中负责从资源池中获取资源的关键方法。这个方法在连接池中起着重要的作用,用于分配可用的资源给应用程序。

以下是大致的checkoutResource(long)方法执行过程:

  1. 首先,方法会检查资源池的状态。如果资源池已关闭,则抛出异常,表示资源池不可用。
  2. 方法会尝试从空闲资源列表中获取一个可用的资源。空闲资源是指当前没有被分配给应用程序的资源。如果有可用的空闲资源,则将其从空闲资源列表中移除,并返回给应用程序。
  3. 如果没有可用的空闲资源,方法会判断当前已分配的资源数量是否达到资源池的最大容量限制。如果已达到最大容量限制,则进入等待状态,直到有资源可用为止。
  4. 如果还有可用的资源(未达到最大容量限制),则根据资源池的配置和策略,创建一个新的资源,并将其分配给应用程序使用。
  5. 分配资源后,方法会更新资源的状态,并进行必要的标记和记录,以确保资源的正确分配和跟踪。
C3P0数据连接池源码-获取连接对象的过程

BasicResourcePool.prelimCheckoutResource(long)

负责预检查和准备资源分配的重要方法。在资源池中分配资源之前,该方法会执行一些预检查和准备工作,以确保分配过程的正确性和可靠性。

以下是大致的prelimCheckoutResource(long)方法执行过程:

  1. 首先,方法会检查资源池的状态。如果资源池已关闭,则抛出异常,表示资源池不可用。
  2. 方法会判断是否需要在获取资源之前执行验证检查。这可以通过配置来决定,例如testConnectionOnCheckout选项。如果需要执行验证检查,会调用BasicResourcePool.checkedOutConnectionTest()方法对资源进行验证。
  3. 如果资源池中存在空闲资源,则直接返回一个空闲资源。这样可以避免创建新的资源对象,提高资源分配的效率。
  4. 如果没有空闲资源可用,则继续执行后续的资源分配逻辑。
  5. 方法会检查当前已分配的资源数量是否达到资源池的最大容量限制。如果已达到最大容量限制,则进入等待状态,直到有资源可用为止。
  6. 如果还有可用的资源(未达到最大容量限制),则根据资源池的配置和策略,创建一个新的资源,并将其标记为已分配状态。
  7. 分配资源后,方法会进行必要的标记和记录,以确保资源的正确分配和跟踪。

BasicResourcePool._recheckResizePool()

重新检查资源池的大小,并根据需要进行动态调整。

在C3P0中,资源池的大小可以根据配置进行自动调整,以适应应用程序的需求。这个自动调整的过程是通过监控资源池中的资源使用情况来实现的。当资源池的使用情况发生变化时,就会触发大小调整机制。

C3P0数据连接池源码-获取连接对象的过程

BasicResourcePool.expandPool(int)

用于扩展资源池的大小。当资源池中的资源不足以满足当前需求时,可以调用该方法来增加资源池的容量。方法的作用是向资源池中添加指定数量的新资源。它接受一个整数参数,表示要添加的资源数量。

C3P0数据连接池源码-获取连接对象的过程

它们的run方法中最终会去调用PooledConnectionResourcePoolManager的acquireResource方法。

PooledConnectionResourcePoolManager.acquireResource()

在该方法中,首先会获取连接池管理器(ResourcePoolManager)的锁,并检查连接池管理器是否已关闭,如果已关闭则抛出异常。

接下来,通过调用ResourcePoolManager#checkoutResource(long)方法来获取连接资源。这个方法是连接池管理器的核心方法,负责从连接池中获取可用的资源。

在获取连接资源之前,会检查连接池管理器的状态以及连接资源是否可用。如果连接资源不可用,会尝试通过ResourcePoolManager#_recheckResizePool()方法来重新检查并调整连接池的大小。

如果连接资源可用,就会标记该资源为已使用,并返回给调用方。

最后,释放连接池管理器的锁,并返回获取到的连接资源。

C3P0数据连接池源码-获取连接对象的过程

WrapperConnectionPoolDataSource.getPooledConnection(String, String, ConnectionCustomizer, String)

用于获取包装后的连接池连接。

该方法接收四个参数:

  • user: 数据库用户名
  • password: 数据库密码
  • customizer: 连接定制器(可选)
  • dataSourceName: 数据源名称(可选)

在方法内部,首先通过调用getPooledConnection(user, password)方法获取到原始的连接池连接(PooledConnection)。

然后,通过调用wrapConnection方法将原始连接包装为一个新的连接对象(WrapperConnection),并返回给调用方。

包装连接的过程可以应用一些定制逻辑,比如连接定制器可以在连接创建后对其进行自定义操作,例如设置连接属性、执行初始化查询等。最后,返回包装后的连接对象。

C3P0数据连接池源码-获取连接对象的过程

(此处已添加书籍卡片,请到今日头条客户端查看)

继续阅读