当前位置: 首页 > 工具软件 > aeon-pool > 使用案例 >

连接池优化之启用PoolPreparedStatements

尉迟明辉
2023-12-01

 DBCP连接池可以缓存PreparedStatement,本质上就是缓存游标。
    一个SQL语句,无论是Insert,Update,Delete还是Select都是游标操作,只不过Select游标指向查询结果,而其余的指向修改的目标。
    除了连接可以缓存,游标也是可以缓存的,主要是避免游标的反复创建。虽然Oracle对完全相同的SQL可以共享执行计划,但是也需要去共享池查询这个SQL的信息(该SQL的Hash值是否在共享池内)。缓存游标,则进一步优化,避免了反复查询共享池的操作(个人臆测).
    首先,做一个实验,证明游标可以反复利用。
    

--创建实验表
create table t as select rownum r from dual connect by level<10;

set serveroutput on

declare 
    cursor cur is select * from t;
    v_record t%rowtype;    
begin
    open cur;
    fetch cur into v_record;
    dbms_output.put_line(v_record.r);
    fetch cur into v_record;
    dbms_output.put_line(v_record.r);
    close cur;
    
    open cur;
    fetch cur into v_record;
    dbms_output.put_line(v_record.r);
    fetch cur into v_record;
    dbms_output.put_line(v_record.r);
    close cur;
    
end;
/

   
    实验结果:
            1
            2
            1
            2

 

可以看到游标在关闭之后,可以重新打开。并且重新打开的游标,与前次打开的游标,在数据上没有任何关系。第一次读到2,重新打开之后,会从1开始,而不是从3开始。

这个代码如果在JAVA程序中,就是这个样子的。

 

  1.        
     Class.forName("oracle.jdbc.OracleDriver");
            Connection conn = DriverManager.getConnection("jdbc:oracle:thin:127.0.0.1:1521:orcl", "edmond", "edmond");
            PreparedStatement cmd = conn.prepareStatement("select * from t");
            //第一次调用
            ResultSet rs = cmd.executeQuery();
            rs.next();
            System.out.println(rs.getString(1));
            rs.next();
            System.out.println(rs.getString(1));
    
            //第二次调用
            rs = cmd.executeQuery();
            rs.next();
            System.out.println(rs.getString(1));
            rs.next();
            System.out.println(rs.getString(1));
            cmd.close();
            conn.close();

    值得注意的是,PreparedStatement就表示Oracle的游标,但是一旦PreparedStatement关闭,就无法重新打开。所以复用PreparedStatement只需要在关闭之前重新调用executeQuery方法即可。
    
    如果连接池启动PoolPreparedStatements,则可能在每一个Connection的代理对象中,包括下面的结构
    Map> poolPreparedStatements
    其中Key是SQL语句或者SQL语句的Hash值,代理的Connection会根据SQL返回一个可用的prepareStatement;如果没有,则会创建新的prepareStatement对象。而这个返回的prepareStatement对象,也同样是代理对象。
    因为在调用连接池返回的prepareStatement的close方法时,不会真正的close这个对象,因为这样就无法实现复用的效果。可能只是修改了这个对象的标志位,标明其可用。

    下面是DBCP连接池开启游标缓存的 代码。
    可以想见 ds.getConnection()返回的Connection和PreparedStatement应该都是代理对象。

private static void testDataSource() throws SQLException {
        BasicDataSource ds = new BasicDataSource();
        ds.setUrl("jdbc:oracle:thin:127.0.0.1:1521:orcl");
        ds.setUsername("edmond");
        ds.setPassword("edmond");
        ds.setPoolPreparedStatements(true);
        ds.setMaxOpenPreparedStatements(300);

        Connection conn = ds.getConnection();
        PreparedStatement cmd = conn.prepareStatement("select * from t");
        ResultSet rs = cmd.executeQuery();
        rs.next();
        System.out.println(rs.getString(1));
        rs.next();
        System.out.println(rs.getString(1));
        cmd.close();
        conn.close();
    }

    另外,Oracle游标对应的是PreparedStatement,而不是ResultSet。
    并且MaxOpenPreparedStatements的设置应该小于Oracle的Open_Cursor的数值。
    

public static void main(String[] args) throws ClassNotFoundException, SQLException {
        List<PreparedStatement> list = new ArrayList<PreparedStatement>();

        Class.forName("oracle.jdbc.OracleDriver");
        Connection conn = DriverManager.getConnection("jdbc:oracle:thin:127.0.0.1:1521:orcl", "edmond", "edmond");
        for (int i = 0; i < 305; i++) {
            PreparedStatement cmd = conn.prepareStatement("select * from t");
            
            ResultSet rs = cmd.executeQuery();
            rs.next();
            rs.close();
            rs = null;
            list.add(cmd);
        }
        conn.close();
    }

    结果出现异常:
Exception in thread "main" java.sql.SQLException: ORA-00604: 递归 SQL 级别 1 出现错误
ORA-01000: 超出打开游标的最大数
ORA-00604: 递归 SQL 级别 1 出现错误
ORA-01000: 超出打开游标的最大数
ORA-01000: 超出打开游标的最大数

 

    可以看到,如果PreparedStatement没有关闭,则Oracle那端的游标就没有释放。
    最终这个连接的游标超过Oracle的open_cursor数值(默认300),就会报错。
    所以启用了PoolPreparedStatements,一定注意设置MaxOpenPreparedStatements小于Oracle Open_Cursor的数值。

 类似资料: