数据库连接池简单介绍和 C3P0的JDBC配置
前面一节我们介绍了怎样利用jdbc连接数据库,已经实现了数据库的连接,可是在实际的项目开发中,能够发现基本上都使用了数据库连接池技术。为什么要使用数据库连接池呢?根源在于对数据库连接的低效管理
答: 普通的JDBC数据库连接,用户请求一次查询的时候就会向数据库发起一次连接。运行完后就断开连接,这种方式会消耗大量的资源和时间。数据库的连接资源并没有得到非常好的重复利用。
若是同一时候有几百人甚至几千人在线。频繁地进行数据库连接操作,这将会占用非常多的系统资源,严重的甚至会造成server的奔溃。这样频繁的创建销毁数据库连接十分耗费时间和资源,并且开发人员也不能非常好的控制数据库的连接数。有可能由于分配的连接过多而导致内存耗尽。
数据库连接池的实现原理解析,以及设计连接池时须要考虑的因素
- 并发问题
首先必须考虑多线程的环境。 能够使用synchronized关键字,lock等就可以确保线程是同步的,public synchronized Connection getConnection() - 多数据库server和多用户
设计一个符合单例模式的连接池管理类。在连接池管理类的唯一实例被创建时读取一个资源文件。当中资源文件里存放着多个数据库的url地址等信息。依据资源文件提供的信息,创建多个连接池类的实例,每个实例都是一个特定数据库的连接池。连接池管理类实例为每个连接池实例取一个名字。通过不同的名字来管理不同的连接池。
对于同一个数据库有多个用户使用不同的名称和密码訪问的情况,也能够通过资源文件处理,即在资源文件里设置多个具有同样url地址,但具有不同username与password的数据库连接信息。 - 事务处理
Connection类本身提供了对事务的支持。能够通过设置connection的autocommit属性为false 然后显式的调用commit或rollback方法来实现。
try{
connect.setAutoCommit(false);
........ 进行的数据库操作语句
connect.commit();
}catch{
connect.rollback();
}finally{
connect.close();
}
可是当2个线程共用一个连接Connection对象,并且各自都有自己的事务要处理时候。对于连接池是一个非常头疼的问题,由于即使Connection类提供了对应的事务支持。可是我们仍然不能确定那个数据库操作是对应那个事务的。这是由于我们有2个线程都在进行事务操作而引起的。但要高效的进行Connection复用,就必须提供对应的事务支持机制。
可採用每个事务独占一个连接来实现,尽管这个方案有点浪费连接池资源。可是能够大大减少事务管理的复杂性。
4. 连接池的分配与释放
合理的分配与释放。能够提高连接的复用度,从而减少建立新连接的开销。同一时候还能够加快用户的訪问速度。
对于连接的管理可使用空暇池。
即把已经创建但尚未分配出去的连接按创建时间存放到一个空暇池中。每当用户请求一个连接时,系统首先检查空暇池内有没有空暇连接。假设有就把建立时间最长(通过容器的顺序存放实现)的那个连接分配给他(实际是先做连接是否有效的推断,假设可用就分配给用户,如不可用就把这个连接从空暇池删掉,又一次检測空暇池是否还有连接)。假设没有则检查当前所开连接池是否达到连接池所同意的最大连接数(maxconn)假设没有达到,就新建一个连接,假设已经达到,就等待一定的时间(timeout)。假设在等待的时间内有连接被释放出来就能够把这个连接分配给等待的用户,假设等待时间超过预定时间timeout 则返回空值(null)。系统对已经分配出去正在使用的连接仅仅做计数,当使用完后再返还给空暇池,可是会发生无法对正在使用的连接进行管理的状况,所以建议使用一个链表存储。
对于空暇连接的状态,可开辟专门的线程定时检測,这样会花费一定的系统开销,但能够保证较快的响应速度。也可採取不开辟专门线程,仅仅是在分配前检測的方法
再分配、释放策略对于有效复用连接非常重要,引用记数模式在复用资源方面用的非常广泛,每个数据库连接,保留一个引用记数,用来记录该连接的使用者的个数,我们对Connection类进行进一步包装来实现引用记数,确定当前被引用多少。详细是哪个用户引用了该连接将在连接池中登记,一旦一个连接被分配出去。那么就会对该连接的申请者进行登记,并且添加引用记数,当被释放回来时候就删除他已经登记的信息,同一时候减少一次引用记数
5、连接池的配置与维护
连接池中究竟应该放置多少连接,才干使系统的性能最佳?系统可採取设置最小连接数(minconn)和最大连接数(maxconn)来控制连接池中的连接。最小连接数是系统启动时连接池所创建的连接数。假设创建过多。则系统启动就慢,但创建后系统的响应速度会非常快;假设创建过少。则系统启动的非常快。响应起来却慢。这样。能够在开发时。设置较小的最小连接数。开发起来会快,而在系统实际使用时设置较大的,由于这样对訪问客户来说速度会快些。最大连接数是连接池中同意连接的最大数目,详细设置多少。要看系统的訪问量。可通过重复測试。找到最佳点。
怎样确保连接池中的最小连接数呢?有动态和静态两种策略。动态即每隔一定时间就对连接池进行检測,假设发现连接数量小于最小连接数。则补充对应数量的新连接以保证连接池的正常运转。静态是发现空暇连接不够时再去检查。
自己实现一个简易的数据库连接池。仅仅考虑了连接池的一些方面
// 连接池类
我在设计的时候须要考虑的几点
1. 首先是数据库连接的存储,要能非常easy的管理和获取数据库连接。将数据库连接分为两部分。一部分是空暇池,一部分是正在使用的数据库连接, 使用LinkedList 实现栈来存储空暇的数据库连接,(优势在于每一次获取到的连接都是新的连接。这种连接基本上都是可用的,基本上不会发生连接不可用导致又一次再去获取连接的操作), 使用LinkedList 实现队列来存储正在使用中数据库连接(优势在于,队列的头部就是眼下使用时间最长的连接,方便进行检查,回收这个使用时间超过限制的数据库连接)
2. 怎样回收分配出去的连接,即当外部的连接调用了close方法之后。怎样让它返回到数据库连接池中。而不是销毁?
方法: 使用动态代理, 当请求一个Connection 时,返回用户一个代理的Connection 对象。这样就能够对close方法进行拦截。调用了close方法。会自己主动运行代理类中的invoke方法, 在invoke方法里面就能够实现对实际连接的一些操作了。 详细实现请查看 getConnection() 方法。
3. 事实上获取连接的时候应当首先检查空暇池是否有空暇连接,再检查空暇连接是否可用,当数据库连接池没有连接的时候,要进行一次性创建新的连接,同一时候要进行检查看能否进行连接的创建,是否达到了最大值等, 所以数据库的一些配置属性须要在静态代码块中通过Properties类读取出来。
public class MyDatabasePool {
private LinkedList<Connection> idlelist; // 使用LinkedList实现栈存储数据库连接,存放的空暇连接
private LinkedList<Connection> usinglist; // 使用LinkedList实现队列存储数据库连接,存放的正在使用的连接
private static Properties props; // 读取配置文件信息
private static int initialPoolSize; // 初始连接池大小
private static int maxPoolSize; // 连接池最大连接数
private static int acquireIncrement; // 无连接时。一次性创建连接数
static {
props = new Properties();
try {
props.load(new FileInputStream("myPool.properties"));
initialPoolSize = Integer.parseInt(props
.getProperty("initialPoolSize"));
maxPoolSize = Integer.parseInt(props.getProperty("maxPoolSize"));
acquireIncrement = Integer.parseInt(props
.getProperty("acquireIncrement"));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 构造函数,在数据库连接池里先创建几个连接
// 我看了一下c3p0的源代码,里面是用的代理连接 new 。而不是真实的物理连接
public MyDatabasePool() throws ClassNotFoundException, SQLException {
idlelist = new LinkedList<Connection>();
usinglist = new LinkedList<Connection>();
Class.forName(props.getProperty("MySQLdriverClass"));
for (int i = 0; i < initialPoolSize; i++) {
Connection conn = DriverManager.getConnection(
props.getProperty("MySQLurl"),
props.getProperty("MySQLusername"),
props.getProperty("MySQLpassword"));
idlelist.addLast(conn);
}
}
// 获取数据库连接
public Connection getConnection() throws SQLException {
if (idlelist.size() > 0) {
usinglist.addFirst(idlelist.getLast()); // 仅仅是获取第一个连接并没有删除
Connection conn = idlelist.removeLast(); // 获取第一个连接并删除
// return conn; //返回真实的物理连接
// 返回一个真实物理连接的动态代理连接对象
return (Connection) Proxy.newProxyInstance(ProxyConnection.class
.getClassLoader(), new Class<?>[] { Connection.class },
new ProxyConnection(conn));
} else {
// 创建新的数据库连接
boolean flag = dynamicIncrement();
if (flag) {
usinglist.add(idlelist.getLast()); // 仅仅是获取第一个连接并没有删除
Connection conn = idlelist.removeLast(); // 获取第一个连接并删除
// return conn; //返回真实的物理连接
// 返回一个真实物理连接的动态代理连接对象
return (Connection) Proxy.newProxyInstance(
ProxyConnection.class.getClassLoader(),
new Class[] { Connection.class }, new ProxyConnection(
conn));
} else {
throw new SQLException("没连接了");
}
}
}
// 连接池里无连接,动态增长
private boolean dynamicIncrement() throws SQLException {
int num = idlelist.size() + usinglist.size();
int num2 = maxPoolSize - num;
// 假设能够创建连接,并且创建的连接数就是acquireIncrement
if (num2 >= acquireIncrement) {
for (int i = 0; i < acquireIncrement; i++) {
Connection conn = DriverManager.getConnection(
props.getProperty("MySQLurl"),
props.getProperty("MySQLusername"),
props.getProperty("MySQLpassword"));
idlelist.addLast(conn);
}
return true;
}
// 假设能够创建连接,可是创建的连接数仅仅能是num2个
if (num2 > 0) {
for (int i = 0; i < num2; i++) {
Connection conn = DriverManager.getConnection(
props.getProperty("MySQLurl"),
props.getProperty("MySQLusername"),
props.getProperty("MySQLpassword"));
idlelist.addLast(conn);
}
return true;
}
return false;
}
// Connection的动态代理类
class ProxyConnection implements InvocationHandler {
private Connection conn;
public ProxyConnection(Connection conn) {
this.conn = conn;
}
// 关闭数据库连接,放回到空暇池中
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// TODO Auto-generated method stub
// 分配出去的代理连接调用了close方法,进行拦截,实现我们自己想要的操作
if (method.getName().equals("close")) {
// conn.close(); // 这一句的话就直接关闭连接了,所以不写
// 应该事先的操作是将 conn 放到空暇池中去,从使用池中移除
System.out.println(idlelist.size());
System.out.println(usinglist.size());
idlelist.addLast(conn);
usinglist.remove(conn);
System.out.println(idlelist.size());
System.out.println(usinglist.size());
return null;
}
// 其它方法仍然调用真实对象的方法
return method.invoke(conn, args);
}
}
}
配置文件 myPool.properties
# mysql database driver
MySQLdriverClass=com.mysql.jdbc.Driver
MySQLurl=jdbc:mysql://127.0.0.1/test?useSSL=false
MySQLusername=root
MySQLpassword=root
initialPoolSize=3
minPoolSize=2
maxPoolSize=50
acquireIncrement=3
JDBC的API中没有提供连接池的方法,所以能够使用例如以下常见的几种数据库连接池:
C3P0:
C3P0是一个开源的JDBC连接池,支持JDBC3规范和JDBC2的标准扩展。
c3p0是异步操作的,缓慢的JDBC操作通过帮助进程完毕。扩展这些操作能够有效的提升性能。眼下使用它的开源项目有Hibernate,Spring等。c3p0有自己主动回收空暇连接功能。稳定性好,大并发量的压力下稳定性也有一定的保证 无连接池监控
c3p0所需jar:
c3p0-0.9.2.1.jar
mchange-commons-java-0.2.3.4.jar
DBCP
是 apache 上的一个 java 连接池项目,也是 tomcat 使用的连接池组件,连接池的基本功能都有,一般不建议使用,无连接池监控
使用dbcp须要2个包:
commons-dbcp.jar
commons-pool.jar
Proxool
Proxool是一种Java数据库连接池技术。Sourceforge下的一个开源项目,这个项目提供一个健壮、易用的连接池,最为关键的是这个连接池提供监控数据库连接的功能,方便易用,便于发现连接泄漏的情况。
Druid
Druid是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池、插件框架和SQL解析器组成。
该项目主要是为了扩展JDBC的一些限制,能够让程序猿实现一些特殊的需求,比方向密钥服务请求凭证、统计SQL信息、SQL性能收集、SQL注入检查、SQL翻译等。程序猿能够通过定制来实现自己须要的功能。
利用C3PO进行配置实现数据库连接池的使用:
第一步: 导入jar包 c3p0-0.9.2.1.jar 和 mchange-commons-java-0.2.3.4.jar
第二步: 书写配置文件 c3p0-config.xml; 里面有一个參数是须要注意的。能够详细看看以下的配置文件
注意的是:
1. 文件名称必须为c3p0-config.xml, 这是由于C3P0会默认读取文件名称为c3p0-config.xml的配置文件进而对数据库连接池进行配置。
2. c3p0-config.xml 必须和你写的java代码在同一个文件夹下,一般就是放在项目的 src文件夹下
<?xml version="1.0" encoding="utf-8"?
>
<c3p0-config>
<!-- c3p0也能够指定配置文件。并且配置文件能够是properties,也可骒xml的。
当然xml的高级一些了。
可是c3p0的配置文件名称必须为c3p0-config.xml。
并且必须放在类路径下 -->
<!-- 默认的配置这里我们默认使用mysql数据库 -->
<default-config>
<!-- 设置数据库的驱动,url, 用户名, 密码 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://127.0.0.1/test?useSSL=false</property>
<property name="user">root</property>
<property name="password">root</property>
<!-- 建立连接池时初始分配的连接池数 = 3-->
<property name="initialPoolSize">3</property>
<!-- 连接池中的最少连接数 = 2 -->
<property name="minPoolSize">2</property>
<!-- 连接池中的最大连接数 = 50-->
<property name="maxPoolSize">50</property>
<!-- 当连接池中连接耗尽时再一次新生成多少个连接 Default: 3 -->
<property name="acquireIncrement">3</property>
<!-- 最大空暇时间,超过多长时间连接自己主动销毁,秒为单位。默觉得0。即永远不会自己主动销毁 -->
<property name="maxIdleTime">1800</property>
<!--每60秒检查全部连接池中的空暇连接。Default: 0 -->
<property name="idleConnectionTestPeriod">60</property>
<!-- c3p0还能够为某个用户设置单独的连接数-->
<user-overrides user="test-user">
<property name="maxPoolSize">10</property>
<property name="minPoolSize">1</property>
<property name="maxStatements">0</property>
</user-overrides>
</default-config>
<!-- c3p0的配置文件里能够配置多个数据库连接信息。能够给每个配置起个名字。这样能够方便的通过配置名称来切换配置信息 -->
<!-- 名字为Oracle-config的配置 -->
<named-config name="Oracle-config">
<property name="driverClass">oracle.jdbc.driver.OracleDriver</property>
<property name="jdbcUrl">jdbc:oracle:thin:@localhost:1521:test</property>
<property name="user">scott</property>
<property name="password">tiger</property>
</named-config>
</c3p0-config>
第三步: 能够自己创建一个工具类。从数据库的连接池中获取连接,我这里仅仅是写了一个建议的连接池工具类,当然你能够依据自己的须要进行扩展
import java.sql.Connection;
import java.sql.SQLException;
import com.mchange.v2.c3p0.ComboPooledDataSource;
// 数据库连接池的工具类
public final class PoolUtil {
private static ComboPooledDataSource ds = null;
static {
// 两种创建数据库连接池的办法
// 第一种: 使用配置文件里的默认配置<default-config>
ds = new ComboPooledDataSource();
// 另外一种: 使用配置文件里设置的其它配置名字的配置 name-config
// ds = new ComboPooledDataSource("Oracle-config");
// 第三种: 我们能够显式的在程序中进行设置数据库连接池的信息
// ds.setDriverClass("com.mysql.jdbc.Driver");
// ds.setJdbcUrl("jdbc:mysql://127.0.0.1/test?useSSL=false");
// ds.setUser("root");
// ds.setPassword("root");
}
// 获取数据库的连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
}
第四步 :进行数据库连接池的測试,观測是否成功使用了连接池
在这里须要注意的一点,当你在程序里使用完数据库连接之后,必须显示的调用close()方法, 此时的close语句并不会关闭与数据库的TCP连接,而是将连接归还回到连接池中去。变为空暇状态, 假设不close掉的话,这个连接将会一直被占用。
public class PoolTest {
public static void main(String[] args) throws SQLException {
Connection conn = null;
try {
conn = PoolUtil.getConnection();
} catch (SQLException e) {
System.out.println("未获取数据库连接");
e.printStackTrace();
}
String sql = "select * from user where id = ?";
PreparedStatement prep = null;
prep = (PreparedStatement) conn.prepareStatement(sql);
prep.setInt(1, 1);
// 查询sql 语句, 返回一个结果集
ResultSet result = prep.executeQuery();
// 处理结果集, 释放资源
while (result.next()) {
System.out.println(result.getInt("id"));
System.out.println(result.getString("name"));
System.out.println(result.getInt("age"));
System.out.println(result.getString("salary"));
}
// 注意的是。即使使用了数据库连接池之后。这里也必须显式的调用close语句,
// 此时的close语句并不会关闭与数据库的TCP连接。而是将连接归还回到池中去,变为空暇状态
// 假设不close掉的话。这个连接将会一直被占用
result.close();
prep.close();
conn.close();
}
}
C3P0的源代码的一些关键解析:
几个关键的类:
C3P0PooledConnectionPoolManager是连接池的管理类,
C3P0PooledConnectionPool是连接池类,
BasicResourcePool是真正管理数据库连接池的类
获取一个连接的代码:
public Connection getConnection() throws SQLException
{
PooledConnection pc = getPoolManager().getPool().checkoutPooledConnection();
return pc.getConnection();
}
public Object checkoutResource(long timeout)
1) 关键步骤代码:Object resc = prelimCheckoutResource(timeout);
查看池中是否有未使用的connection,有就返回(还要推断是否空暇、是否过期);没有,假设没有达到最大数,就生成一个。或者就等待。
2) 关键步骤代码:
boolean refurb = attemptRefurbishResourceOnCheckout (resc);
得到连接后,检測连接的可用性。
3) 连接可用,接着推断连接是否处于管理中。不在就再调用本方法获取一个。在就返回本连接。
C3P0**从连接池拿到的连接都是代理的连接。一个对PooledConnection类型对象的代理对象,所以能够放心调用close方法。仅仅是连接进行了归还。不会关闭物理连接 并且C3p0中实际创建的对象是实现了PooledConnection(接口,位于javax.sql)。 它本身包括Connection,和这个connection相关的全部Statement,Result,能够实现对连接的一些管理。加入监听器等。 所做的全部数据库操作,都被PooledConnection所管理。
**c3p0默认的实现是NewPooledConnection
C3P0使用了LinkedList来存放空暇池中的连接,每次取连接的时候是get(0), 然后remove(0) 应该是使用的队列来实现的空暇池
C3P0 使用的HashMap 来存放的正在使用的连接,这样方便进行查找,连接返回的时候,依据连接能够高速的定位到要remove的那个连接
C3P0 使用HashSet来存储一些失效的,可是仍旧被使用或者检查的资源。
这些数据结构能够在 BasicResourcePool 类中查看到
本博客參考了
http://blog.csdn.net/shuaihj/article/details/14223015