日期:2014-05-16  浏览次数:20502 次

Oracle JDBC内存管理(Oracle JDBC Memory Management)

摘要:从Oracle10g开始在JDBC驱动中,增加了对执行每个Statement的缓存。目的就是对相同的SQL缓存其查询结果,从而提高查询的性能。这种缓存形式,在服务端做的比较多,但是Oracle是通过JDBC将结果缓存在离应用程序最近的地方(客户端),不知道其他数据库是否有同样的功能。

一、 缓存的意义
为什么要通过Statement来缓存数据,这样做的前提是PreparedStatement或者是CallableStatement而不是Statement,因为前两者数据库可以根据用户提交的SQL做预编译,只要是SQL语句是一样的,数据库就认为是同一次查询,数据库就可以根据查询结果做缓存或者优化。那什么样的SQL语句数据库认为是一样的呢?如下面:

Select * from userinfo where username = ‘junshan’ and id = 1

Select * from userinfo where username = ‘bobo’  and id = 2

显然这两条SQL语句是不一样的,如果你通过下面这样执行

statement.execute(“Select * from userinfo where username = ‘junshan’ and id = 1”)

或者

statement.execute(Select * from userinfo where username = ‘bobo’ and id = 2)

数据库完全认为这是两个毫不相干的SQL操作,不仅要重新解释这个SQL,构造SQL语法树,语法树的优化,准备执行环境等等。所有这些操作都用做一遍。但是很明显这两个SQL除了参数不一样,其他都是一样的,而对数据库来说,唯一不同的是,只要在第一次查询出来的结果集中根据参数值重新过滤一下即可,而前面的SQL解释,构建语法树,甚至数据都可能不要在数据库中重新捞一把。为此JDBC提供一种根据理想的PreparedStatement、和其子类CallableStatement。他们就是将单纯的SQL语句和SQL中的参数值分开,然后将值再映射过去。关于映射这一部分可以参数《深入分析Ibatis框架之系统架构与映射原理》。这样数据库就可以根据单纯的SQL做更多优化。上面的SQL应该这样写更为合理

Connection conn=DriverManager.getConnection(url,user,password);

java.sql.PreparedStatementst = conn.prepareStatement(Select * from userinfo where username = ?  and
id = ?);

st.setString(0,’junshan’);

st.setInt(1,1);

st.execute();

前面说到都是在数据库服务端,但是在Oracle10g以后的JDBC中,在客户端以做了数据的缓存,这里的缓存只是针对和一个PreparedStatement对应的数据的缓存。这样做就可以让相同的SQL的执行完全不需要请求道数据库,达到更好的性能,但是这样必然会给客户端增加内存负担,在Oracle的官方说明中也提到了可能会导致严重的内存问题。

二、 Oracle JDBC驱动如何缓存
下面看一下Oracle JDBC客户端如何缓存:

客户端可以指定缓存一定数量的Statement,每个Statement持有两个buffers,一个是byte[]一个是char[]。这些buffers又在一个叫做Implicit Statement Cache的cache中。char[]主要是存储char类型的数据,而byte[]存储所有其他类型的数据。

当一个Statement被执行时这来两个buffer就将被创建,由于buffer创建是在获取数据之前,所以是不知道应该创建多大的buffer来存查询返回结果,为此只能按照Statement中申明的字段类型来分配buffer大小。分配的规则就是声明的字段类占用的字节数乘以最大的存储数量如VARCHAR2(10)就是10*2 bytes其他的类型如NUMBER、DATE都是安装22bytes分配还有一些如CLOB、BLOB存储的是数据地址,最大地址空间能达到4k所以至少分配4K给他们。

例如下面的SQL分配的buffer大小是:


CREATE TABLE TAB (ID NUMBER(10), NAME VARCHAR2(40), DOB DATE)

ResultSet r = stmt.executeQuery(“SELECT * FROM TAB”);

22 + (40 * 2) +22 = 124 bytes,这只是一行数据的buffer大小,Oracle JDBC默认是取十行,那分配的大小就是1240 bytes了。

按照这种分配方式是很容易出现内存问题的,加入有多个列都是都是VARCHAR2(4000)声明的字段,如果缓存100行,那么内存就很可能被用光了。

Oracle的建议是:

小心的定义每列的数据类型和最大的数据大小如不要每个VARCHAR2都需要定义为VARCHAR2(4000);还有就是设置fetchSize这个参数,就是分配多少行的缓存量,如有的SQL最多都是查询一行的数据你设置10行,那就是完全的浪费。

通常单个Statement引起的内存问题可能性有但是很小,最大的可能就是客户端会同时创建多个Statement并执行,还有一个潜在的问题就是缓存住Statement而不关闭,通常当Statement被关闭时这个 Statement持有的buffer也就被释放。但是如果被缓存的Statement都是比较大的数据很可能会撑爆内存。如果发现每条Statement分配的buffer较多,但是缓存的Statement数量有很多的话,就应该将缓存Statement的数量调小一点。

通常这个配置是下面的选项:

<datasources>

<local-tx-datasource>

<jndi-name>DB1DataSource_cm2</jndi-name>

<connection-url>jdbc:oracle:oci:@tbdb1_cm2</connection-url>

<connection-property name=”SetBigStringTryClob”>true</connection-property>

<connection-property name=”defaultRowPrefetch”>50</connection-property>

<driver-class>oracle.jdbc.driver.OracleDriver</driver-class>

<min-pool-size>2</min-pool-size>

<max-pool-size>3</max-pool-size>

<idle-timeout-minutes>10</idle-timeout-minutes>

<prepared-statement-cache-size>75</prepared-statement-cache-size>

<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name>

<metadata><type-mapping>Oracle9i</type-mapping></metadata>

<user-name>taobao</user-name>

<password>taobao</password>