数据库连接池简介和 C3P0的JDBC配置

前面一节我们介绍了如何利用jdbc连接数据库,已经实现了数据库的连接,但是在实际的项目开发中,可以发现基本上都使用了数据库连接池技术,为什么要使用数据库连接池呢?根源在于对数据库连接的低效管理
答: 普通的JDBC数据库连接,用户请求一次查询的时候就会向数据库发起一次连接,执行完后就断开连接,这样的方式会消耗大量的资源和时间,数据库的连接资源并没有得到很好的重复利用。若是同时有几百人甚至几千人在线,频繁地进行数据库连接操作,这将会占用很多的系统资源,严重的甚至会造成服务器的奔溃。这样频繁的创建销毁数据库连接十分耗费时间和资源,而且开发者也不能很好的控制数据库的连接数,有可能因为分配的连接过多而导致内存耗尽。

数据库连接池的实现原理解析,以及设计连接池时需要考虑的因素

  1. 并发问题
    首先必须考虑多线程的环境, 可以使用synchronized关键字,lock等即可确保线程是同步的,public synchronized Connection getConnection()
  2. 多数据库服务器和多用户
    设计一个符合单例模式的连接池管理类,在连接池管理类的唯一实例被创建时读取一个资源文件,其中资源文件中存放着多个数据库的url地址等信息。根据资源文件提供的信息,创建多个连接池类的实例,每一个实例都是一个特定数据库的连接池。连接池管理类实例为每个连接池实例取一个名字,通过不同的名字来管理不同的连接池。
    对于同一个数据库有多个用户使用不同的名称和密码访问的情况,也可以通过资源文件处理,即在资源文件中设置多个具有相同url地址,但具有不同用户名和密码的数据库连接信息。
  3. 事务处理
    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. 首先是数据库连接的存储,要能很容易的管理和获取数据库连接,将数据库连接分为两部分,一部分是空闲池,一部分是正在使用的数据库连接使用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章