iBatis应用--控制执行SQL时的超时时间

来源:互联网 发布:php对象的使用 编辑:程序博客网 时间:2024/04/27 09:19

执行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 SQLException;

设置当前语句执行时间的方法,时间的单位是秒,当设置值为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 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的配置文件只能在程序启动时加载,或者说当iBatisSqlMapClient对象在构造结束时,配置文件已被解析、加载完成,而后续使用时并不会加载发生变动的部分。而我总不能为了调整这个超时值,不停的启动、停止应用,这在实际部署中是有困难的。有没有什么方法可以让我在程序不停止运行的条件下,动态调整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执行时的超时时长限制。

参考资料:

http://www.tutorialspoint.com/ibatis/index.htm 

2013年1月2日17:37:50

上述的样例中有些小的错误,不过很好改,所以留给有心人去修改吧。