一、数据库连接池概述
1.1 为什么使用数据库连接池
如果用户每次请求都向数据库获得连接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、拓机。如下图所示:
1.2 数据库连接池是什么
数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标。数据库连接池正式针对这个问题提出来的。
数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。如下图所示:
有了池,我们就不用自己来创建Connection,而是通过池来获取Connection对象。当使用完Connection后,调用Connection的close()方法也不会真的关闭Connection,而是把Connection“归还”给池。池就可以再利用这个Connection对象了。
数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中, 这些数据库连接的数量是由最小数据库连接数来设定的.无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量.连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中.
数据库连接池的最小连接数和最大连接数的设置要考虑到以下几个因素:
- 最小连接数:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费.
- 最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作
- 如果最小连接数与最大连接数相差很大:那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接.不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,他将被放到连接池中等待重复使用或是空间超时后被释放.
二、自定义数据库连接池
2.1 初始版本
【实现思路】
- 编写连接池需实现java.sql.DataSource接口,并重写接口中的getConnection()方法
- 提供一个集合,用于存放连接,因为移除/添加操作过多,所以选择LinkedList
- 在静态代码块中为连接池初始化5个连接
- 程序如果需要连接,调用实现类的getConnection(),从连接池(容器List)获得连接。为保证当前连接只能提供一个线程使用,需要将连接先从连接池中移除。
- 使用完连接,要释放资源时,不执行close()方法,而是将连接添加到连接池中
【代码编写】
public class MyDataSource implements DataSource {
//1.创建1个容器用于存储Connection对象
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//2.创建5个连接放到容器中去
static{
for (int i = 0; i < 5; i++) {
Connection connection = JdbcUtils.getConnection();
pool.add(connection);
}
}
/**
* 重写获取连接的方法
*/
@Override
public Connection getConnection() throws SQLException {
Connection connection = null;
//3.使用前先判断
if (pool.size() == 0) {
//4.池子里面没有,我们再创建一些
for (int i = 0; i < 5; i++) {
connection = JdbcUtils.getConnection();
pool.add(connection);
}
}
//5.从池子里面获取一个连接对象Connection
connection = pool.remove(0);
return connection;
}
/**
* 归还连接对象到连接池中去
*/
public void backConnection(Connection connection) {
pool.add(connection);
}
...
}
【测试】
@Test
public void testAddUser() {
Connection conn = null;
PreparedStatement pstmt = null;
// 1.创建自定义连接池对象
MyDataSource dataSource = new MyDataSource();
try {
// 2.从池子中获取连接
conn = dataSource.getConnection();
String sql = "insert into t_user values(?,?)";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 3);
pstmt.setString(2, "张三");
int num = pstmt.executeUpdate();
if (num > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
dataSource.backConnection(conn);
}
}
2.2 方法增强——装饰者模式
【需求】
上述自定义连接池中存在严重问题,用户调用getConnection()获得连接后,必须使用backConnection()方法进行连接的归还,如果用户调用conn.close()将连接真正的释放,连接池中将出现无连接可用。
此时我们希望,即使用户调用了close()方法,连接仍归还给连接池。close()方法原有功能是释放资源,我们期望的功能是将连接归还给连接池。说明close()方法没有我们希望的功能,我们将对close()方法进行增强,从而实现将连接归还给连接池的功能。
【方法增强总结】
1.继承:
- 子类继承父类,将父类的方法进行复写,从而进行增强。
- 使用前提:必须有父类,且存在继承关系。
2.装饰者模式:
- 此设计模式专门用于增强方法。
- 使用前提:必须有接口
3.动态代理:
- 在运行时动态的创建代理类,完成增强操作。与装饰者相似
- 使用前提:必须有接口
- 难点:需要反射技术
【装饰者设计模式】
设计模式:专门为解决某一类问题,而编写的固定格式的代码。
固定结构:接口A,已知实现类C,需要装饰者创建代理类B
实现步骤:
- 创建类B,并实现接口A
- 提供类B的构造方法,参数类型为A,用于接收A接口的其他实现类(C)
- 给类B添加类型为A的成员变量,用于存放A接口的其他实现类
- 增强需要的方法
- 实现不需要增强的方法,方法体重调用成员变量存放的其他实现类对应的方法
A a = ...C;
B b = new B(a);
class B implements A{
private A a;
public B(A,a){
this.a = a;
}
// 增强的方法
public void close(){
}
// 不需要增强的方法
public void commit(){
this.a.commit();
}
}
2.3 使用装饰者模式增强连接池
【装饰类】
//1.实现同一个接口Connection
public class MyConnection implements Connection {
//3.定义一个变量
private Connection conn;
private LinkedList<Connection> pool;
//2.编写一个构造方法(参数使用了面向对象的多态特性)
public MyConnection(Connection conn,LinkedList<Connection> pool) {
this.conn = conn;
this.pool = pool;
}
//4.书写需要增强的方法
@Override
public void close() throws SQLException {
pool.add(conn);
}
/**
* 5.实现不需要增强的方法
* 注意:此方法必须覆盖!否则会出现空指针异常!!!
*/
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return conn.prepareStatement(sql);
}
@Override
public void commit() throws SQLException {
}
...
}
【使用装饰类】
public class MyDataSource1 implements DataSource {
//1.创建1个容器用于存储Connection对象
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//2.创建5个连接放到容器中去
static{
for (int i = 0; i < 5; i++) {
Connection conn = JdbcUtils.getConnection();
//放入池子中connection对象已经经过改造了
MyConnection myconn = new MyConnection(conn, pool);
pool.add(myconn);
}
}
/**
* 重写获取连接的方法
*/
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
//3.使用前先判断
if (pool.size() == 0) {
//4.池子里面没有,我们再创建一些
for (int i = 0; i < 5; i++) {
conn = JdbcUtils.getConnection();
//放入池子中connection对象已经经过改造了
MyConnection myconn = new MyConnection(conn, pool);
pool.add(myconn);
}
}
//5.从池子里面获取一个连接对象Connection
conn = pool.remove(0);
return conn;
}
/**
* 归还连接对象到连接池中去
*/
public void backConnection(Connection connection) {
pool.add(connection);
}
...
}
【测试】
/**
* 使用改造过的connection
*/
@Test
public void testAddUser1() {
Connection conn = null;
PreparedStatement pstmt = null;
// 1.创建自定义连接池对象
DataSource dataSource = new MyDataSource1();
try {
// 2.从池子中获取连接
conn = dataSource.getConnection();
String sql = "insert into t_user values(?,?)";
//3.必须在自定义的connection类中重写prepareStatement(sql)方法
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 6);
pstmt.setString(2, "王五");
int num = pstmt.executeUpdate();
if (num > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//这里的connection是已经经过增强的,增强后的conn.close()就是将连接归还给连接池
JdbcUtils.release(conn, pstmt, null);
}
}
三、DBCP连接池
DBCP 是 Apache 软件基金组织下的开源连接池实现,在企业开发中也比较常见,它是tomcat内置的连接池
3.1 导包
- Commons-dbcp.jar:连接池的实现
- Commons-pool.jar:连接池实现的依赖库
3.2 提供配置文件
- 配置文件名称:*.properties
- 配置文件位置:任意,建议src(classpath/类路径)
#基本配置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb1
username=root
password=123
#初始化池大小,即一开始池中就会有10个连接对象
默认值为0
initialSize=0
#最大连接数,如果设置maxActive=50时,池中最多可以有50个连接,当然这50个连接中包含被使用的和没被使用的(空闲)
#你是一个包工头,你一共有50个工人,但这50个工人有的当前正在工作,有的正在空闲
#默认值为8,如果设置为非正数,表示没有限制!即无限大
maxActive=8
#最大空闲连接
#当设置maxIdle=30时,你是包工头,你允许最多有20个工人空闲,如果现在有30个空闲工人,那么要开除10个
#默认值为8,如果设置为负数,表示没有限制!即无限大
maxIdle=8
#最小空闲连接
#如果设置minIdel=5时,如果你的工人只有3个空闲,那么你需要再去招2个回来,保证有5个空闲工人
#默认值为0
minIdle=0
#最大等待时间
#当设置maxWait=5000时,现在你的工作都出去工作了,又来了一个工作,需要一个工人。
#这时就要等待有工人回来,如果等待5000毫秒还没回来,那就抛出异常
#没有工人的原因:最多工人数为50,已经有50个工人了,不能再招了,但50人都出去工作了。
#默认值为-1,表示无限期等待,不会抛出异常。
maxWait=-1
#连接属性
#就是原来放在url后面的参数,可以使用connectionProperties来指定
#如果已经在url后面指定了,那么就不用在这里指定了。
#useServerPrepStmts=true,MySQL开启预编译功能
#cachePrepStmts=true,MySQL开启缓存PreparedStatement功能,
#prepStmtCacheSize=50,缓存PreparedStatement的上限
#prepStmtCacheSqlLimit=300,当SQL模板长度大于300时,就不再缓存它
connectionProperties=useUnicode=true;characterEncoding=UTF8;useServerPrepStmts=true;cachePrepStmts=true;prepStmtCacheSize=50;prepStmtCacheSqlLimit=300
#连接的默认提交方式
#默认值为true
defaultAutoCommit=true
#连接是否为只读连接
#Connection有一对方法:setReadOnly(boolean)和isReadOnly()
#如果是只读连接,那么你只能用这个连接来做查询
#指定连接为只读是为了优化!这个优化与并发事务相关!
#如果两个并发事务,对同一行记录做增、删、改操作,是不是一定要隔离它们啊?
#如果两个并发事务,对同一行记录只做查询操作,那么是不是就不用隔离它们了?
#如果没有指定这个属性值,那么是否为只读连接,这就由驱动自己来决定了。即Connection的实现类自己来决定!
defaultReadOnly=false
#指定事务的事务隔离级别
#可选值:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
#如果没有指定,那么由驱动中的Connection实现类自己来决定
defaultTransactionIsolation=REPEATABLE_READ
3.3 编写工具类
public class DBCPUtils {
/**
* 在java中,编写数据库连接池需实现java.sql.DataSource接口,每一种数据库连接池都是DataSource接口的实现
* DBCP连接池就是java.sql.DataSource接口的一个具体实现
*/
private static DataSource dataSource = null;
//在静态代码块中创建数据库连接池
static{
try {
//加载dbcp.properties配置文件
InputStream in = DBCPUtils.class.getClassLoader().getResourceAsStream("dbcp.properties");
Properties prop = new Properties();
prop.load(in);
//创建数据源
dataSource = BasicDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从数据源中获取数据库连接
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 释放资源
* 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
*/
public static void release(Connection conn, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3.4 测试DBCP数据源
public class DBCPTest {
@Test
public void addUser() {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 获取数据库连接
conn = DBCPUtils.getConnection();
String sql = "insert into t_user values(?,?)";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 8);
pstmt.setString(2, "刘备");
int num = pstmt.executeUpdate();
if (num > 0) {
System.out.println("添加成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
四、C3P0连接池
C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。C3P0数据源在项目开发中使用得比较多。
c3p0与dbcp区别:
- dbcp没有自动回收空闲连接的功能
- c3p0有自动回收空闲连接功能
4.1 导包
- c3p0-0.9.2-pre1.jar
- mchange-commons-0.2.jar
如果操作的是Oracle数据库,那么还需要导入c3p0-oracle-thin-extras-0.9.2-pre1.jar
4.2 添加配置文件
- 配置文件名称:c3p0-config.xml(必须是这个)
- 配置文件位置:src(必须放在类路径下)
- 配置文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<!--这是默认配置信息-->
<default-config>
<!--连接四大参数配置-->
<property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">root</property>
<property name="password">root</property>
<!--池参数配置-->
<!--如果池中数据连接不够时一次增长多少个-->
<property name="acquireIncrement">3</property>
<!--初始化连接数-->
<property name="initialPoolSize">10</property>
<!--最小连接数-->
<property name="minPoolSize">2</property>
<!--最大连接数-->
<property name="maxPoolSize">10</property>
</default-config>
<!--专门为Oracle提供的配置信息-->
<named-config name="oracle-config">
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">root</property>
<property name="password">123</property>
<property name="acquireIncrement">3</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">2</property>
<property name="maxPoolSize">10</property>
</named-config>
</c3p0-config>
4.3 编写工具类
public class C3P0Utils {
private static ComboPooledDataSource dataSource = null;
//在静态代码块中创建数据库连接池
static{
try {
// 通过代码创建C3P0连接池
/*dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUser("root");
dataSource.setPassword("root");
dataSource.setInitialPoolSize(10);
dataSource.setMinPoolSize(5);
dataSource.setMaxPoolSize(20);*/
//通过读取C3P0的xml配置文件创建数据源,C3P0的xml配置文件c3p0-config.xml必须放在src目录下
//使用C3P0的默认配置来创建数据源
dataSource = new ComboPooledDataSource();
//使用名为oracle-config的配置来创建数据源
// dataSource = new ComboPooledDataSource("oracle-config");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从数据源中获取数据库连接
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 释放资源
* 释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象
*/
public static void release(Connection conn, Statement st, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
4.4 测试C3P0数据源
@Test
public void addUser() {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 获取数据库连接
conn = C3P0Utils.getConnection();
String sql = "insert into t_user values(?,?)";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 9);
pstmt.setString(2, "关羽");
int num = pstmt.executeUpdate();
if (num > 0) {
System.out.println("添加成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
五、Tomcat配置连接池
在实际开发中,我们有时候还会使用服务器提供给我们的数据库连接池,比如我们希望Tomcat服务器在启动的时候可以帮我们创建一个数据库连接池,那么我们在应用程序中就不需要手动去创建数据库连接池,直接使用Tomcat服务器创建好的数据库连接池即可。要想让Tomcat服务器在启动的时候帮我们创建一个数据库连接池,那么需要简单配置一下Tomcat服务器。
5.1 JNDI技术简介
JNDI(Java Naming and Directory Interface),Java命名和目录接口,它对应于J2SE中的javax.naming包,
这套API的主要作用在于:它可以把Java对象放在一个容器中(JNDI容器),并为容器中的java对象取一个名称,以后程序想获得Java对象,只需 通过名称检索即可。其核心API为Context,它代表JNDI容器,其lookup方法为检索容器中对应名称的对象。
Tomcat服务器创建的数据源是以JNDI资源的形式发布的,所以说在Tomat服务器中配置一个数据源实际上就是在配置一个JNDI资源,通过查看Tomcat文档,我们知道使用如下的方式配置tomcat服务器的数据源:
<Context>
<Resource name="jdbc/datasource" auth="Container"
type="javax.sql.DataSource" username="root" password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test"
maxActive="8" maxIdle="4"/>
</Context>
服务器创建好数据源之后,我们的应用程序又该怎么样得到这个数据源呢,Tomcat服务器创建好数据源之后是以JNDI的形式绑定到一个JNDI容器中的,我们可以把JNDI想象成一个大大的容器,我们可以往这个容器中存放一些对象,一些资源,JNDI容器中存放的对象和资源都会有一个独一无二的名称,应用程序想从JNDI容器中获取资源时,只需要告诉JNDI容器要获取的资源的名称,JNDI根据名称去找到对应的资源后返回给应用程序。我们平时做javaEE开发时,服务器会为我们的应用程序创建很多资源,比如request对象,response对象,服务器创建的这些资源有两种方式提供给我们的应用程序使用:第一种是通过方法参数的形式传递进来,比如我们在Servlet中写的doPost和doGet方法中使用到的request对象和response对象就是服务器以参数的形式传递给我们的。第二种就是JNDI的方式,服务器把创建好的资源绑定到JNDI容器中去,应用程序想要使用资源时,就直接从JNDI容器中获取相应的资源即可。
对于上面的name="jdbc/datasource"数据源资源,在应用程序中可以用如下的代码去获取
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
dataSource = (DataSource)envCtx.lookup("jdbc/datasource");
此种配置下,数据库的驱动jar文件需放置在tomcat的lib下
5.2 配置JNDI资源
- 在Web项目的WebRoot目录下的META-INF目录创建一个context.xml文件
- 在context.xml文件配置tomcat服务器的数据源
<Context>
<!--name:指定资源的名称
factory:用来创建资源的工厂,这个值基本上是固定的,不用修改
type:资源的类型
其他的东西都是资源的参数
-->
<Resource
name="jdbc/datasource"
factory="org.apache.naming.factory.BeanFactory"
type="javax.sql.DataSource"
username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test"
maxActive="8"
maxIdle="4"/>
</Context>
5.3 获取JNDI的资源
/**
* 获取JNDI的资源
*
*/
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/*
* 1. 创建JNDI的上下文对象
*/
try {
Context cxt = new InitialContext();
// 2查询出入口
// Context envContext = (Context)cxt.lookup("java:comp/env");
// 3. 再进行二次查询,找到我们的资源
// 使用的是名称与<Resource>元素的name对应
// DataSource dataSource = (DataSource)envContext.lookup("jdbc/dataSource");
DataSource dataSource = (DataSource)cxt.lookup("java:comp/env/jdbc/dataSource");
Connection con = dataSource.getConnection();
System.out.println(con);
con.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}