执行SQL时,需要关注其执行时花费的时间,避免SQL执行过长的时间,占用数据库的资源。对于OLTP类型的系统,SQL的执行时间一般来说都是比较短的,假如某个SQL执行时间超出平均值,则需要给予特别的关注,分析下是正常情况还是代码问题。
SQL执行时间过长,会有哪些后果呢?假如查询操作频率不高,偶尔才会执行一次,那么最多只会占用一个数据库链接,并且在执行成功之后,程序打开的数据库链接对象即会被释放,这样对应用系统不会有什么影响。假如执行频率很高的话,可能会过快的占满全部数据库链接,导致其它数据库访问操作不能正常执行,如果发生这种现象的话,就会导致整个系统短时间内出现假死的现象,而这是绝对不可接受的。
那么有什么办法可以控制SQL的执行时长吗?在JDBC的标准中,java.sql.Statement接口定义了SQL执行的超时时长相关的方法,如下:
查询当前语句执行时长的方法,时间的单位是秒,当返回值为0时,表明当前没有限制。
/**
* Retrieves the number of seconds the driver will
* wait for a <code>Statement</code> object to execute.
* If the limit is exceeded, a
* <code>SQLException</code> is thrown.
*
* @return the current query timeout limit in seconds; zero means there is
* no limit
* @exception SQLException if a database access error occurs or
* this method is called on a closed <code>Statement</code>
* @see #setQueryTimeout
*/
int getQueryTimeout() throws
设置当前语句执行时间的方法,时间的单位是秒,当设置值为0时,表明不需要控制SQL的执行时长。
/**
* Sets the number of seconds the driver will wait for a
* <code>Statement</code> object to execute to the given number of seconds.
*By default there is no limit on the amount of time allowed for a running
* statement to complete. If the limit is exceeded, an
* <code>SQLTimeoutException</code> is thrown.
* A JDBC driver must apply this limit to the <code>execute</code>,
* <code>executeQuery</code> and <code>executeUpdate</code> methods.
* <p>
* <strong>Note:</strong> JDBC driver implementations may also apply this
* limit to {@code ResultSet} methods
* (consult your driver vendor documentation for details).
* <p>
* <strong>Note:</strong> In the case of {@code Statement} batching, it is
* implementation defined as to whether the time-out is applied to
* individual SQL commands added via the {@code addBatch} method or to
* the entire batch of SQL commands invoked by the {@code executeBatch}
* method (consult your driver vendor documentation for details).
*
* @param seconds the new query timeout limit in seconds; zero means
* there is no limit
* @exception SQLException if a database access error occurs,
* this method is called on a closed <code>Statement</code>
* or the condition seconds >= 0 is not satisfied
* @see #getQueryTimeout
*/
void setQueryTimeout(int seconds) throws
注释写的非常详细,并且没有歧义,非常值得每一个开发人员学习。
从注释可以得到如下信息:
1. 这两个方法在使用时,数据库提供的驱动代码可能会抛出异常,类型为SQLException;
2. 某个SQL语句在执行时的超时时长默认是不做限制的,即使用getQueryTimeout方法查询超时时长时,默认值为0;
3. 当SQL执行时间超出了限制还没有结束,则驱动代码将抛出异常SQLTimeoutException;
4. 数据库的驱动在实现标准时,需要保证针对查询方法实现超时抛出异常的特性;
5. 对于一个事务内批量提交的SQL,数据库驱动可以选择性的实现控制查询超时的特性;
6. 对于不同的数据库驱动,SQL语句的超时限制也可以应用到ResultSet接口定义的方法;
那么当我们使用iBatis来访问数据库时,如何控制SQL执行的超时时长呢?iBatis的开发人员早就想到了这点,并且提供了方法。根据文档,有两种方法:
1. SqlMapConfig.xml中定义的全局超时变量,这样即使定义SQL时没有配置超时值,也有一个总开关来控制,避免开发人员忘记。样例如下(设置超时时间为900秒):
<settings
cacheModelsEnabled="true"
enhancementEnabled="true"
lazyLoadingEnabled="true"
errorTracingEnabled="true"
maxRequests="32"
maxSessions="10"
maxTransactions="5"
useStatementNamespaces="false"
defaultStatementTimeout="900"
/>
2. Sql配置文件中针对单个SQL定义的超时值,这样当全局定义的超时值不能满足需求时,还要以依照SQL的特点进行定制。样例如下(设置超时时间为20秒):
<procedure id="test_exec_proc" parameterClass="map" timeout="20">
{call p_timeout(?)}
</procedure>
想来有这两种方法应当是够用了。但总有贪心的人,比如我,想出来一些奇怪的场景,发现iBatis文档里的方法解决不了。那么是什么场景呢?项目组开发了一些生成报表数据的存储过程,这些存储过程长度不一,有些比较短,有些非常长。那么当然了,存储过程的执行时间和自身的长度是关的。开发时不可能预见到别人写的存储过程在执行时究竟需要多长时间,考虑到存储过程是人开发的,代码中存在一些影响执行时间的问题再正常不过,因此这些存储在执行时长必须要有限制,而这个限制多长合适,就是一个有意思的话题。而且在不同的环境、不同的数据量等条件下,相同的存储过程的执行时间其实是不定的,就需要针对不同的环境和数据量,设定不同的上限。但这下我就犯难了,因为iBatis的配置文件只能在程序启动时加载,或者说当iBatis的SqlMapClient对象在构造结束时,配置文件已被解析、加载完成,而后续使用时并不会加载发生变动的部分。而我总不能为了调整这个超时值,不停的启动、停止应用,这在实际部署中是有困难的。有没有什么方法可以让我在程序不停止运行的条件下,动态调整iBatis配置的SQL的执行时长?
解决问题的方法还是需要iBatis来提供。文档里没有的,就从源代码里找。开源软件的好处就在于此,代码是大家可见的,只要花点时间,那么方法也就是可见的了。
从源码中可以发现,实现我们通常使用的SqlMapClient接口对象,其实现类实现了两个接口,除众所周知的SqlMapClient外,还有一个ExtendedSqlMapClient,但却被标记为@deprecated,即避免使用。接口SqlMapClient中包括了一些常规方法,没有我们需要的。那么ExtendedSqlMapClient会有什么特别的吗?
这个ExtendedSqlMapClient接口的定义看起来和接口SqlMapClient类似,但多出来不少特别的方法,如下:
MappedStatement getMappedStatement(String s);
boolean
boolean
SqlExecutor getSqlExecutor();
SqlMapExecutorDelegate getDelegate();
其中getMappedStatement比较特别,从名字看,应当可以指的是配置文件中定义的动态SQL的信息。查看MappedStatement类的实现代码,果然,这个类里除了访问数据的实现代码外,还包括有SQL配置文件中全部信息的定义,当然也包括了超时值的访问方法,如下:
public
{
return timeout;
}
public void
{
this.timeout
}
这样一来,假如在执行存储过程的SQL前,获取存储过程对应的动态SQL对象,修改其超时值,然后执行SQL,这样就达到了我们之前期望的动态修改SQL执行超时的要求。但是不是这样呢?接下来写一段样例来验证想法。
为了模拟SQL执行时间比较长的现象,这里在iBatis中调用存储过程,在存储过程中调用一个延时函数,人为构造出SQL执行时间超长的现象,存储过程的样例如下:
grant execute on dbms_lock to username;--为用户增加dbms_lock对象的执行权限
create or replace procedure p_timeout is
begin
dbms_lock.sleep(30);
end p_timeout;
/
同时应用文档中给出的超时值定义方法,给出动态SQL的默认执行时长,动态SQL的配置样例如下:
<procedure id="test_exec_proc" parameterClass="map" timeout="20">
{call p_timeout(?)}
</procedure>
测试代码如下(省略了类定义和异常处理):
public static void main(final String[] args) throws
final InputStream stream = Resources.getResourceAsStream("SqlMapConfig.xml");
final
final MappedStatement stmt = sqlClient.getMappedStatement("test.timeout");
out.println("default timeout : "
stmt.setTimeout(5);
out.println("after changed : "
final long
try
"test.timeout");
}
catch (final
}
out.println("sql execute time, cost : "
}
这样就达到了之前的预期,在重启应用的条件下,修改了SQL执行时的超时时长限制。
参考资料:
http://www.tutorialspoint.com/ibatis/index.htm
2013年1月2日17:37:50
上述的样例中有些小的错误,不过很好改,所以留给有心人去修改吧。