Java DataSource

数据源,简单理解为数据源头,提供了应用程序所需要数据的位置。数据源保证了应用程序与目标数据之间交互的规范和协议,它可以是数据库,文件系统等等。其中数据源定义了位置信息,用户验证信息和交互时所需的一些特性的配置,同时它封装了如何建立与数据源的连接,向外暴露获取连接的接口。应用程序连接数据库无需关注其底层是如何如何建立的,也就是说应用业务逻辑与连接数据库操作是松耦合的。 以下只讨论当数据源为数据库的情况,且为 Java 环境下 JDBC 规范下的如何建立与数据库的连接,其它情况类似。

DriverManager vs DataSource

  • DriverManager 是 JDBC 2.0 之前用来获取数据库连接的唯一方法。
  • DataSource 是 JDBC 2.0 之后(含)用来获取数据库连接的首选方法。

使用 DriverManager 获取数据库连接:

使用 DataSource 获取数据库连接:

我们知道 JDBC 的核心原理是 java.sql.Driver 驱动接口和各个数据库厂商提供的 java.sql.Driver 接口实现类,比如 MySQL 实现类为 com.mysql.jdbc.Driver。通过 DriverManager 获取数据库连接的一般步骤为:

  1. Class.forName("com.mysql.jdbc.Driver"),注册数据库厂商的 JDBC 驱动(static 初始化块)。
  2. DriverManager.getConnection(url, user, pass),根据 url、user、pass 信息获取数据库连接。

url 的格式与具体的数据库厂商有关,而 user 是用来认证的用户名,pass 则是用来认证的用户名密码。

DataSource 只是一个接口,DataSource 是数据源的意思,我更喜欢将 DataSource 理解为数据库的抽象表示,我们可以从这个 DataSource 中获取数据库连接,这和数据库系统很相似。因为 DataSource 是数据库的抽象表示(其本身也是一个接口),所以我们需要一个 DataSource 实现类才能使用 DataSource 来获取数据库连接,那么这个实现类由谁提供呢?当然是数据库厂商了(MySQL、Oracle 等)。

DataSource 是 DriverManager 的更高层次的抽象。一个 DataSource 实例就表示一个数据库,我们可以从一个 DataSource 对象中获取数据库连接,即 dataSource.getConnection()。因为 DataSource 实现类是由数据库厂商提供的,所以数据库厂商可以自由的在实现类中加入数据库连接池、分布式事务等功能,而我们使用者只需要简单的调用 dataSource.getConnection() 来获取数据库连接,也因为如此,DataSource 的性能要比 DriverManager.getConnection() 获取数据库连接的方式好得多,因为 DataSource 实现类内部完全可以加入自己的数据库连接池逻辑,但这一点都不影响 dataSource 实例的使用方法。

DataSource 实现类必须提供一个无参构造函数,这么规定的原因是为了可以方便的与 JNDI 一起使用,因为通常情况下,我们都会在 JNDI 容器中注册对应的 DataSource 实现类,这样我们在程序中只需要像获取 Spring IoC 容器中的 bean 实例一样简单,如 DataSource dataSource = context.lookup("name"),是不是感觉和 IoC 容器差不多?当然将 dataSource 放到 JNDI 中的目的和使用 IoC 容器的目的也是一样的,就是为了降低不同对象之间的耦合度。使用 JNDI 后,我们如果要更换 DataSource 实现,只需要修改容器的配置文件,然后重启容器就行了,不用更改任何代码(Tomcat 容器就有 JNDI 功能,这个以后再了解)。

javax.sql.DataSource 接口的定义:

有必要声明一点,DataSource 并不能取代 DriverManager,它只是 DriverManager 的封装,底层可能仍然是使用 DriverManager 来获取连接的。之所以要封装是因为 DriverManager 获取连接的方式太直接、太底层了,无法透明的实现数据库连接池,因为每次调用 DriverManager.getConnection() 返回的数据库连接都是新创建的,不能复用。而 DataSource 就不一样了,DataSource 只是一个抽象接口,该接口暴露的 API 为 dataSource.getConnection(),当我们调用 dataSource.getConnection() 获取数据库连接时,实现类内部完全可以进行别的操作,比如实现数据库连接池,又比如分布式事务支持等。

还有一点就是,DataSource 实现类不一定要数据库厂商支持才能做,我们自己也是能够实现 DataSource 接口的,并且还能够在里面实现数据库连接池等,同理,DataSource 也有很多开源实现,比如 Apache 的 DBCP、号称性能无敌的 HikariCP、功能全面的 druid(阿里巴巴开发)。DBCP 目前分为两个版本,1.x 和 2.x(有没有感觉和 Log4J 很相似,1.x 和 2.x),DBCP 1.x 性能听说不行,是单线程同步模型。而 DBCP 2.x 推出比较晚,当它推出时,很多项目早就转为了其他数据库连接池实现了。而 druid 是阿里开源的一个数据库连接池实现,听说性能和 HikariCP 不相上下,不过因为 druid 很多功能是为了方便运维而开发的,所以目前使用比较普遍的数据库连接池实现是 HikariCP。

注意,DataSource 实现类并不都是带有数据库连接池功能的,请不要混淆了。Spring JDBC 框架里面也有一个 DriverManagerDataSource 实现,不带有数据库连接池功能,它只是 DriverManager 的简单封装而已。

最后说下 JNDI,JNDI 全称为 Java Naming and Directory Interface,中文为“Java 命名和目录接口”。千万不要以为这是个什么很高级很复杂的协议/接口,你完全可以将它看作是 Spring IoC 容器的“JDK 官方版本”,本质上,它们都是一个“容器”,都是存储对象的容器,而且都支持在 xml 配置文件中配置“对象”,然后在程序中可以使用对应的 name 来获取对应的对象,仅此而已。常见的 Tomcat 容器就支持 JNDI 功能。

MySQL DriverManager 例子

MySQL DataSource 例子

MySQL Spring-JDBC-DataSource 例子

DataSource 总结

其实通过上面三个 MySQL 的例子,可以知道,使用数据库厂商提供的 DataSource 实现的耦合度其实并没有降低,反而有点增高了,而使用开源实现的通用 DataSource 实现(上面的 DriverManagerDataSource 是 spring-jdbc 模块自带的一个简单 DriverManager 封装实现)的耦合度其实是最低的,而且通常这些 DataSource 实现都有连接池功能,建议大家使用开源的 DataSource 实现,如 DBCP、HikariCP、druid。注意,Spring-JDBC 模块里面的 DriverManager 仅仅是 DriverManager 接口的封装而已,并没有实现数据库连接池,每次调用 getConnection() 方法获取的数据库连接也都是新创建的。

HikariCP 使用

pom.xml

HikariCP 是第二代数据库连接池,并且号称是最快的 Java 数据库连接池,其中 Hikari 的中文意思是“光”,寓意就是这个连接池很快,像光速一样。这里主要讲解如何在 spring.xml 中配置 HikariCP 的 datasource 数据源 bean,常见配置如下:

上述配置的意思如下:HikariCP 在启动后,会立即与数据库建立 30 个连接,如果一个连接的总生命时长超过 2 小时,则被释放(使用中的连接会等使用者用完之后才会被释放),当连接池中的 30 个连接都被客户端占用之后,新的客户端请求新的连接将会等待 10 秒,如果 10 秒之内其它客户端释放了一个连接,那么当前客户端将获取这个连接并返回,如果 10 秒之后仍然没有获取到可用的连接,则 HikariCP 会抛出 SqlException 异常。所以可以知道,默认情况下 HikariCP 是一个固定大小的连接池,大小就是 maximumPoolSize 指定的值,默认为 10。

注意,有些人会设置一个 idleTimeout 空闲连接的超时时间,但是注意,如果要让 idleTimeout 设置生效,还必须指定 minimumIdle 的值,且 minimumIdle 的值需要小于 maximumPoolSize 才能让这个 idleTimeout 生效,而默认情况下,minimumIdle 与 maximumPoolSize 的值是相同的。minimumIdle 表示连接池中需要至少有多少个空闲连接,如果空闲连接数小于此值且池中总连接数小于 maximumPoolSize,那么 HikariCP 将会建立新的连接,直到空闲连接达到 minimumIdle 指定的值。但是官方文档也明确指出了,不建议设置这个值,如果不设置,那么默认值和 maximumPoolSize 相同,此时 idleTimeout 设置实际上并不会生效,也就是说,官方建议按照我上面的那个配置例子这样用,即作为一个固定大小的连接池来用。