最近监理的一个项目老是出现数据库莫名断开的问题,后来查明原因是由于使用的开发的数据库连接池出现的问题,那我就简单谈谈什么是数据库连接池及期原理。
对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用,情况就完全不同了。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。
连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。
对于共享资源,有一个很著名的设计模式:资源池。该模式正是为了解决资源频繁分配、释放所造成的问题的。把该模式应用到数据库连接管理领域,就是建立一个数据库连接池,提供一套高效的连接分配、使用策略,最终目标是实现连接的高效、安全的复用。
数据库连接池的基本原理是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。如:
外部使用者可通过getConnection 方法获取连接,使用完毕后再通过releaseConnection 方法将连接返回,注意此时连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。
数据库连接池技术带来的优势:
1. 资源重用
由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量)。
2. 更快的系统响应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。
3. 新的资源分配手段
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术,几年钱也许还是个新鲜话题,对于目前的业务系统而言,如果设计中还没有考虑到连接池的应用,那么…….快在设计文档中加上这部分的内容吧。某一应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源。
4. 统一的连接管理,避免数据库连接泄漏
在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用连接。从而避免了常规数据库连接操作中可能出现的资源泄漏。一、数据库连接池
1. 什么是连接池
传统的开发模式下,Servlet处理用户的请求,找Dao查询数据,dao会创建与数据库之间的链接,完成数据查询后会关闭数据库的链接。
这样的方式会导致用户每次请求都要向数据库建立链接而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、宕机。
解决方案就是数据库连接池
连接池就是数据库连接对象的一个缓冲池
我们可以先创建10个数据库连接缓存在连接池中,当用户有请求过来的时候,dao不必创建数据库连接,而是从数据库连接池中获取一个,用完了也不必关闭连接,而是将连接换回池子当中,继续缓存
使用数据库连接池可以极大地提高系统的性能
2. 实现数据库连接池
jdbc针对数据库连接池也定义的接口java.sql.DataSource,所有的数据库连接池实现都要实现该接口
该接口中定义了两个重载的方法
Connection getConnection()
Connection getConnection(String username, String password)
数据库连接池实现思路
1) 定义一个类实现java.sql.DataSource接口
2) 定义一个集合用于保存Connection对象,由于频繁地增删操作,用LinkedList比较好
3) 实现getConnection方法,在方法中取出LinkedList集合中的一个连接对象返回
注意:
返回的Connection对象不是从集合中获得,而是删除
用户用完Connection,会调用close方法释放资源,此时要保证连接换回连接池,而不是关闭连接
重写close方法是难点,解决方案: 装饰设计模式、动态代理
二、 数据源
通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。
一些开源组织提供了数据源的独立实现,常用的有:
DBCP 数据库连接池
C3P0 数据库连接池
1. DBCP 数据源
介绍
DBCP 是 Apache 软件基金组织下的开源连接池实现
tomcat服务器就是使用DBCP作为数据库连接池
使用DBCP数据源,需要导入两个jar包
Commons-dbcp.jar:连接池的实现
Commons-pool.jar:连接池实现的依赖库
DBCP核心 API
BasicDataSource
数据源实现
BasicDataSourceFactory
用于创建数据源的工厂类
dbcp 创建连接池
方法1: 直接创建对象,设置参数
BasicDataSource bds = new BasicDataSource();
// 设置连接数据库需要的配置信息
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl("jdbc:mysql://localhost:3306/jdbc3");
bds.setUsername("root");
bds.setPassword("root");
// 设置连接池的参数
bds.setInitialSize(5);
bds.setMaxActive(10);
ds = bds
方法2: 通过工厂类创建对象,读取配置文件
try {
Properties prop = new Properties();
// 读配置文件
InputStream in =
JdbcUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
prop.load(in);
ds = BasicDataSourceFactory.createDataSource(prop);
}catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
配置文件为dbcpconfig.properties
#连接设置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc3
username=root
password=root
#<!-- 初始化连接 -->
initialSize=5
#最大连接数量
maxActive=10
#<!-- 最大空闲连接 -->
maxIdle=10
#<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
maxWait=60000
#JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
#注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
connectionProperties=useUnicode=true;characterEncoding=gbk
#指定由连接池所创建的连接的自动提交(auto-commit)状态。
defaultAutoCommit=true
#driver default 指定由连接池所创建的连接的只读(read-only)状态。
#如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
defaultReadOnly=
#driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
#可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=READ_UNCOMMITTED
2. C3P0 数据源
介绍
c3p0是一个开源的jdbc连接池,我们熟悉的 Hibernate和 Sprint 框架使用的都是该数据源
在实际中,我们的很多操作都是需要由多条sql来共同完成的,例如,A账户给B账户转账就会对应两条sql
update account set money=money-100 where name=‘a’;
update account set money=money+100 where name=‘b’;
假设第一条sql成功了,而第二条sql失败了,这样就会导致a账户损失了100元,而b账户并未得到100元
如果将两条sql放在一个sql中,当第二条语句失败时,第一条sql语句也同样不会生效,
这样a账户就不会有任何的损失
创建连接池对象
方法1:直接创建对象,设置参数
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc3");
cpds.setUser("root");
cpds.setPassword("root");
cpds.setInitialPoolSize(5);
cpds.setMaxPoolSize(15);
方法2:读取配置文件
ComboPooledDataSource cpds = new ComboPooledDataSource("itcast");
配置文件为c3p0-config.xml 该文件需要放在类路径下
<c3p0-config>
<default-config>
<!—- 默认配置 –->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">15</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbc3</property>
<property name="user">root</property>
<property name="password">root</property>
</default-config>
<named-config name="xwh">
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">15</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbc3</property>
<property name="user">root</property>
<property name="password">root</property>
</named-config>
</c3p0-config>
三、ResultSetMetaData对象
元数据,可以理解为描述数据的数据
jdbc中的元数据是指数据库、表、列的定义信息
ResultSetMetaData对象表示结果集 ResultSet对象的元数据
获得该对象:
ResultSetMetaData metaData = rs.getMetaData();
常用方法:
getColumnCount() 返回resultset对象的列数
getColumnName(int column) 获得指定列的名称
getColumnTypeName(int column) 获得指定列的类型
四、jdbc优化
使用jdbc对数据库进行crud操作时,会有很多重复的代码,仔细分析不难发现其实变化的只是其中几行代码
对于 cud(增删改) 操作,代码几乎完全一样, 唯一的区别就是sql语句不同,我们完全可以把相同的代码抽取出来定义在一个工具方法中,然后定义一个参数来接收sql语句
对于 r(查询) 操作,除SQL语句不同之外,根据操作的实体不同,对ResultSet结果集的处理也有所不相同,因此可义一个query方法,除以参数形式接收变化的SQL语句外,可以使用策略模式由qurey方法的调用者决定如何把ResultSet中的数据映射到实体对象中
优化后的工具类 JdbcUtils
// 通用的增删改方法
public static int update(String sql, Object[] params) throws SQLException {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 获得连接
conn = getConnection();
// 预编译sql
pstmt = conn.prepareStatement(sql);
// 将参数设置进去
for(int i=0; params!=null&&i<params.length; i++) {
pstmt.setObject(i+1, params[i]);
}
// 发送sql
int num = pstmt.executeUpdate();
return num;
} finally {
// 释放资源
release(conn, pstmt, rs);
}
}
// 优化查询
public static Object query(String sql, Object[] params, ResultSetHandler rsh) throws SQLException {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 获得连接
conn = getConnection();
// 预编译sql
pstmt = conn.prepareStatement(sql);
// 将参数设置进去
for(int i=0; params!=null&&i<params.length; i++) {
pstmt.setObject(i+1, params[i]);
}
// 发送sql
rs = pstmt.executeQuery();
// 不知道别人想如何处理结果集
// 干脆想别人所要一个结果集的处理器
// 为了让当前代码继续,定义一个结果集处理器接口
// 策略模式, 规定算法,具体的算法留给将来的调用者实现
Object obj = rsh.handle(rs);
return obj;
} finally {
// 释放资源
release(conn, pstmt, rs);
}
}
结果集处理器接口
public interface ResultSetHandler {
// 处理结果集的方法
public Object handle(ResultSet rs);
}
实现类:
BeanListHandler
public class BeanListHandler implements ResultSetHandler {
private Class clazz;
public BeanListHandler(Class clazz) {
this.clazz = clazz;
}
public Object handle(ResultSet rs) {
try {
// 取出结果集所有的记录,封装到bean,存入list返回
List list = new ArrayList();
while (rs.next()) {
Object bean = clazz.newInstance();
// 获得元数据
ResultSetMetaData metaData = rs.getMetaData();
// 获得列的数量
int count = metaData.getColumnCount();
// 遍历列
for(int i=1; i<=count; i++) {
// 取列名
String columnName = metaData.getColumnName(i);
// 取这列的值
Object value = rs.getObject(columnName);
// 反射出属性
Field field = clazz.getDeclaredField(columnName);
// 设置属性
field.setAccessible(true);
field.set(bean, value);
}
// 加入list
list.add(bean);
}
return list;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
BeanHandler
public class BeanHandler implements ResultSetHandler {
private Class clazz;
public BeanHandler(Class clazz) {
this.clazz = clazz;
}
public Object handle(ResultSet rs) {
// 不知道有几列数据,不知道列名,不知道封装到什么样的bean
// 表的列明和javabean的字段名一致
try {
if(rs.next()) {
// 创建bean
Object bean = clazz.newInstance();
// 封装数据
// 获得结果集的元数据
ResultSetMetaData metaData = rs.getMetaData();
int count = metaData.getColumnCount();
// 迭代取每一列的数据
for(int i=1; i<=count; i++) {
// 获得列名 username
String columnName = metaData.getColumnName(i);
// 获得数据 ddd
Object value = rs.getObject(columnName);
// 根据列名反射出映射的属性 username
Field field = clazz.getDeclaredField(columnName);
// 为属性赋值
field.setAccessible(true);
field.set(bean, value);
}
return bean;
}
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
ArrayHandler
// 取出第一行的所有记录存入一个Object数组
public class ArrayHandler implements ResultSetHandler {
public Object handle(ResultSet rs) {
try {
if (rs.next()) {
// 指向了第一行的记录
// 获得元数据
ResultSetMetaData metaData = rs.getMetaData();
// 获得列数
int count = metaData.getColumnCount();
// 创建数组
Object[] arr = new Object[count];
// 迭代所有列的值,存入数组
for(int i=1; i<=count; i++) {
Object value = rs.getObject(i); // 获得指定列的值
arr[i-1] = value;
}
return arr;
}
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**************************************************************************************************装饰类的实现,后面部分函数的实现同close()方法相同*****************************************/
package dbcp;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.sql.CallableStatement;
import java.sql.DatabaseMetaData;
import java.sql.SQLWarning;
import java.util.Map;
import java.sql.Savepoint;
/*
public class ConnectionDecorator implements Connection {
}
/******************************************************************************************************************************************************/
package dbcp;
import java.sql.Connection;
import java.sql.SQLException;
/**
public class PooledConnection extends ConnectionDecorator{
}
/**********************************************************************************************连接池类的实现*******************************************************/
package dbcp;
import java.sql.*;
import java.util.*;
/*
public class DBConnectPool {
}
/*************************测试类*********************************************/
package dbcp;
import java.sql.*;
public class Test {