在Oracle数据库中,SQL解析有几种:

  • 硬解析,过多的硬解析在系统中产生shared pool latch和library cache liatch争用,消耗过多的shared pool,使得系统不具有可伸缩性。
  • 软解析,过多的软解析仍然可能会导致系统问题,特别是如果有少量的SQL高并发地进行软解析,会产生library cache latch或者是share方式的mutex争用。
  • 软软解析,其实这也也属于软解析,与普通的软解析不同的是,软软解析的SQL会在会话的cached cursor中命中。
  • 一次解析,多次执行,这是解析次数最少的方式,也是系统最具有可扩展性的方式。

那么在JAVA开发的应用中,怎么样才能实现上述第4种方式?如果是循环处理某种数据,这个比较容易实现。其实对于不是这种情况,Oracle也提供了很好的方式来实现这一点。下面是一个例子(例子代码文件为TestStmtCache.java)。

import java.sql.*;
import oracle.jdbc.driver.OracleConnection;

public class TestStmtCache {
    public static Connection getConnection() throws Exception {
        String driver = "oracle.jdbc.driver.OracleDriver";
        String url = "jdbc:oracle:thin:@localhost:1521:xj11g";
        Class.forName(driver);
        return DriverManager.getConnection(url, "test", "test");
    }

    public static void main(String args[]) {
        Connection conn = null;
        try {
            conn = getConnection();
            conn.setAutoCommit(false);
            ((OracleConnection)conn).setStatementCacheSize(0);
            for (int i=0; i <200; i++) {
                testNoCache(conn);
            }
            ((OracleConnection)conn).setStatementCacheSize(20);
            ((OracleConnection)conn).setImplicitCachingEnabled(true);

            for (int i=0; i <200; i++) {
                testCache(conn);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public static void testCache(Connection conn) {
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("select /*cache_test1 */ * from t1 where rownum<=1");
            pstmt.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                pstmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static void testNoCache(Connection conn) {
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("select /*nocache_test1 */ * from t1 where rownum<=1");
            pstmt.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                pstmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

编译上述代码文件TestStmtCache.java,并运行:

E:\JavaCode>set CLASSPATH=.;ojdbc14.jar

E:\JavaCode>d:\works\Java\jdk1.5.0_21\bin\javac TestStmtCache.java

E:\JavaCode>d:\works\Java\jdk1.5.0_21\jre\bin\java TestStmtCache

在数据库中进行查询:

SYS@xj11g> select sql_id,parse_calls,executions,sql_text from v$sqlarea where sql_text like '%cache_test%' and sql_text not like '%v$%';

SQL_ID        PARSE_CALLS  EXECUTIONS SQL_TEXT
------------- ----------- ----------- ------------------------------------------------------------
3nbu9qp40ptjk         200         200 select /*nocache_test1 */ * from t1 where rownum< =1
47hja0fwmmb6c           1         200 select /*cache_test1 */ * from t1 where rownum<=1

可以看到,这两条SQL语句,都执行了200次,但是标记为"nocache_test1"的SQL没有进行语句缓存,其parse calls为200次,即解析了200次,其中一次是硬解析。而标记为"cache_test1"的SQL语句,使用了语句缓存,但是parse calls只有1次,即只有一次硬解析,执行了200次。这里关键的代码在于:

            ((OracleConnection)conn).setStatementCacheSize(20);
            ((OracleConnection)conn).setImplicitCachingEnabled(true);

上述第一行代码设置语句缓存大小,当然20比较偏小,对于比较大型的系统来说,设到200-300比较合适,不过这会耗用一定数量的JAVA内存。这个数值表示一个连接能够缓存多少语句。第二行代码是设置隐式打开语句缓存,也即自动会对PreparedStatement的SQL语句进行缓存。

那么,上述的方式无疑是比较简单的,但是这种方式有一个问题就是,缓存的利用效率可能不高,因为JAVA会将不常用的SQL语句也进行了缓存。Oracle的JDBC驱动也提供了一种手工控制的方式:
将测试代码中的第22行替换为:

((OracleConnection)conn).setExplicitCachingEnabled(true);

第40行替换为:

pstmt = ((OracleConnection)conn).getStatementWithKey ("cache_test1");
if (pstmt==null)

第46行替换为:

((OraclePreparedStatement)pstmt).closeWithKey ("cache_test1");

这样通过手工编码的方式控制哪些语句需要缓存,哪些不需要。
关于语句缓存(Statement Caching)可以参考Oracle在线文档:Statement and Result Set Caching

在JAVA使用JDBC连接数据库进行操作时,有可能为了避免语句运行超时,而设置超时时长。

这里有一个问题是,假设一个事务有两条DML SQL语句,会话在执行第一条SQL成功后,执行第二条SQL超时而中止时,事务处于什么样的状态?通常来说,一个SQL报错,只会进行语句级回滚,整个事务不会进行回滚。我们来进行一下测试。

测试环境:Oracle 11.2.0.2 for Windows。
首先在TEST用户下生成测试表和数据。

TEST@xj11g> create table t1 ( id number primary key,name varchar2(20));

表已创建。

TEST@xj11g> create table t2 ( id number primary key,name varchar2(20));

表已创建。

TEST@xj11g> insert into t2 values (1,'a');

已创建 1 行。

TEST@xj11g> commit;

提交完成。

编译如下的JAVA代码(TestTimeout.java):

import java.sql.*;

public class TestTimeout {
    public static Connection getConnection() throws Exception {
        String driver = "oracle.jdbc.driver.OracleDriver";
        String url = "jdbc:oracle:thin:@localhost:1521:xj11g";
        Class.forName(driver);
        return DriverManager.getConnection(url, "test", "test");
    }

    public static void main(String args[]) {
        test1();
    }
    public static void test1() {
        Connection conn = null;
        Statement pstmt = null;
        try {
            conn = getConnection();
            conn.setAutoCommit(false);
            pstmt = conn.createStatement();
            pstmt.setQueryTimeout(60);
            pstmt.execute("insert into t1 values (1,'xx')");
            pstmt.execute("update t2 set name='x' where id=1");
            conn.commit();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                Thread.sleep(300000);
            } catch (Exception f) {
            }
        } finally {
            try {
                pstmt.close();
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

编译JAVA代码:

E:\JavaCode>d:\works\java\jdk1.5.0_21\bin\javac TestTimeout.java

在一个会话里面将T2表中ID=1的行进行UPDATE操作,但是不提交。

TEST@xj11g> update t2 set name='y' where id=1;

已更新 1 行。

然后运行测试用JAVA程序:

E:\JavaCode>set CLASSPATH=ojdbc14.jar;.

E:\JavaCode>d:\works\java\jdk1.5.0_21\jre\bin\java TestTimeout

可以预见的是,在执行update表T2时会被锁住:

SYS@xj11g> select sid,event,sql_id from v$session where sid=206;

        SID EVENT                         SQL_ID
----------- ----------------------------- -------------
        206 enq: TX - row lock contention 3b3b7s22dv13t

一会儿后JAVA程序报如下的错误:

java.sql.SQLException: ORA-01013: 用户请求取消当前的操作

        at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:112)
        at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:331)
        at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:288)
        at oracle.jdbc.driver.T4C8Oall.receive(T4C8Oall.java:743)
        at oracle.jdbc.driver.T4CStatement.doOall8(T4CStatement.java:207)
        at oracle.jdbc.driver.T4CStatement.executeForRows(T4CStatement.java:946)
        at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1160)
        at oracle.jdbc.driver.OracleStatement.executeInternal(OracleStatement.java:1679)
        at oracle.jdbc.driver.OracleStatement.execute(OracleStatement.java:1645)
        at TestTimeout.test1(TestTimeout.java:23)
        at TestTimeout.main(TestTimeout.java:12)

可以看到,JAVA程序自己CANCEL掉了SQL的执行。
在JAVA代码中长时间的SLEEP是为了便于观察SQL报错后,事务的状态。

SYS@xj11g> select xidusn,xidslot,xidsqn,status from v$transaction where ses_addr=(select saddr from v$session where sid=206);

     XIDUSN     XIDSLOT      XIDSQN STATUS
----------- ----------- ----------- ----------------
          4          28        4117 ACTIVE

SYS@xj11g> select * from v$lock where sid=206;

  SID TY         ID1         ID2       LMODE     REQUEST       CTIME       BLOCK
----- -- ----------- ----------- ----------- ----------- ----------- -----------
  206 AE         100           0           4           0         203           0
  206 TM       63075           0           3           0         203           0
  206 TX      262172        4117           6           0         203           0
SYS@xj11g> select owner,object_name,object_type from dba_objects where object_id=63075;

OWNER    OBJECT_NAME  OBJECT_TYPE
-------- ------------ ------------
TEST     T1           TABLE

可以看到JAVA程序的会话(此时还没断开连接),仍然是在事务中,并且在表T1上加了锁。
此时如果向T1表插入ID值为1的数据,会被锁阻塞。

SYS@xj11g> insert into test.t1 values (1,'xx');

不过奇怪的是,过几分钟后(JAVA中的SLEEP 300000表示休眠300秒,即5分钟),上述的INSERT语句报错了。查询发现JAVA代码中的INSERT的数据提交了:

insert into test.t1 values (1,'xx')
*
第 1 行出现错误:
ORA-00001: 违反唯一约束条件 (TEST.SYS_C0011027)

SYS@xj11g> select * from test.t1;

         ID NAME
----------- --------------------
          1 xx

其实想想就能明白,这个并不奇怪,因为JAVA代码的最后正常断开了数据库连接,在默认情况下,正常断开的数据库连接会自动提交没有提交的事务。这也是一种很难排查的产生数据不一致的原因之一。

小结:这个测试JAVA程序,可以表明,JDBC中的语句超时,只会使当前的SQL中止运行,但如果是在一个事务中,之前运行的DML语句并没有提交。这造成的后果有两种:
1. 如果是连接池,那么超时之前更新的数据可能会被其他请求重用时得以提交,或者是在连接释放时得以提交,这造成数据的不一致,因为不是一个逻辑上有效的事务。
2. 由于数据被更新而没有及时回滚,可能会导致应用重新发起相同的事务时被锁住。如果被锁住的会话仍然有超时中止的机制,那么这种情况就会越来越严重。

为了避免以上的两种问题,在超时后,应该主动发起一次ROLLBACK操作。

最后是JAVA程序测试时数据库会话的SQL TRACE记录:

PARSING IN CURSOR #356760536 len=33 dep=0 uid=52 oct=6 lid=52 tim=36160405140 hv=2229109881 ad='7ff55ce8608' sqlid='3b3b7s22dv13t'
update t2 set name='x' where id=1
END OF STMT
PARSE #356760536:c=0,e=675,p=0,cr=0,cu=0,mis=1,r=0,dep=0,og=1,plh=2789547903,tim=36160405139

*** 2013-09-09 00:51:01.836
WAIT #356760536: nam='enq: TX - row lock contention' ela= 63181195 name|mode=1415053318 usn<<16 | slot=589851 sequence=4123 obj#=63077 tim=36223613008
EXEC #356760536:c=0,e=63208180,p=0,cr=2,cu=1,mis=0,r=0,dep=0,og=1,plh=2789547903,tim=36223613365
ERROR #3:err=1013 tim=36223613426
STAT #356760536 id=1 cnt=0 pid=0 pos=1 obj=0 op='UPDATE  T2 (cr=0 pr=0 pw=0 time=4 us)'
STAT #356760536 id=2 cnt=1 pid=1 pos=1 obj=63078 op='INDEX UNIQUE SCAN SYS_C0011028 (cr=1 pr=0 pw=0 time=10 us cost=1 size=25 card=1)'
WAIT #356760536: nam='SQL*Net break/reset to client' ela= 3586 driver id=1952673792 break?=0 p3=0 obj#=63077 tim=36223617262
WAIT #356760536: nam='SQL*Net message to client' ela= 2 driver id=1952673792 #bytes=1 p3=0 obj#=63077 tim=36223617407

*** 2013-09-09 00:56:01.903
WAIT #356760536: nam='SQL*Net message from client' ela= 300048871 driver id=1952673792 #bytes=1 p3=0 obj#=63077 tim=36523666376
CLOSE #356760536:c=0,e=62,dep=0,type=0,tim=36523666818
XCTEND rlbk=0, rd_only=0, tim=36523666956

在上面可以看到,在最后的确有一个事务事交(XCTEND rlbk=0, rd_only=0)。