分类: Java
2012-12-26 19:11:12
执行SQL时,需要关注其执行时花费的时间,避免SQL执行过长的时间,占用数据库的资源。对于OLTP类型的系统,SQL的执行时间一般来说都是比较短的,假如某个SQL执行时间超出平均值,则需要给予特别的关注,分析下是正常情况还是代码问题。
SQL执行时间过长,会有哪些后果呢?假如查询操作频率不高,偶尔才会执行一次,那么最多只会占用一个数据库链接,并且在执行成功之后,程序打开的数据库链接对象即会被释放,这样对应用系统不会有什么影响。假如执行频率很高的话,可能会过快的占满全部数据库链接,导致其它数据库访问操作不能正常执行,如果发生这种现象的话,就会导致整个系统短时间内出现假死的现象,而这是绝对不可接受的。
那么有什么办法可以控制SQL的执行时长吗?在JDBC的标准中,java.sql.Statement接口定义了SQL执行的超时时长相关的方法,如下:
查询当前语句执行时长的方法,时间的单位是秒,当返回值为0时,表明当前没有限制。
/** * Retrieves the number of seconds the driver will * wait for a * If the limit is exceeded, a * * * @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 * @see #setQueryTimeout */ int getQueryTimeout() throws SQLException; |
设置当前语句执行时间的方法,时间的单位是秒,当设置值为0时,表明不需要控制SQL的执行时长。
/** * Sets the number of seconds the driver will wait for a * *By default there is no limit on the amount of time allowed for a running * statement to complete. If the limit is exceeded, an * * A JDBC driver must apply this limit to the * * * Note: JDBC driver implementations may also apply this * limit to {@code ResultSet} methods * (consult your driver vendor documentation for details). * * Note: 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 * or the condition seconds >= 0 is not satisfied * @see #getQueryTimeout */ void setQueryTimeout(int seconds) throws SQLException; |
注释写的非常详细,并且没有歧义,非常值得每一个开发人员学习。
从注释可以得到如下信息:
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 isLazyLoadingEnabled(); boolean isEnhancementEnabled(); SqlExecutor getSqlExecutor(); SqlMapExecutorDelegate getDelegate(); |
其中getMappedStatement比较特别,从名字看,应当可以指的是配置文件中定义的动态SQL的信息。查看MappedStatement类的实现代码,果然,这个类里除了访问数据的实现代码外,还包括有SQL配置文件中全部信息的定义,当然也包括了超时值的访问方法,如下:
public Integer getTimeout() { return timeout; } public void setTimeout(Integer timeout) { this.timeout = 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 Exception { final InputStream stream = Resources.getResourceAsStream("SqlMapConfig.xml"); final ExtendedSqlMapClient sqlClient = (ExtendedSqlMapClient) SqlMapClientBuilder.buildSqlMapClient(stream); final MappedStatement stmt = sqlClient.getMappedStatement("test.timeout"); System.out.println("default timeout : " + stmt.getTimeout()); stmt.setTimeout(5); System.out.println("after changed : " + stmt.getTimeout()); final long start = System.currentTimeMillis(); try { sqlClient.queryForList("test.timeout"); } catch (final SQLTimeoutException e) { } System.out.println("sql execute time, cost : " + (System.currentTimeMillis() - start)); } |
这样就达到了之前的预期,在重启应用的条件下,修改了SQL执行时的超时时长限制。
参考资料:
2012年12月30日19:08:48
上述部分代码仅是样例,昨天晚上验证功能的时候发现有点小问题,留给有心人去解决吧。