Ibatis之RowHandler

假设一种场景:帐户表中有1千万个帐户,现需要对这1千万个帐户进行结息操作,要求基于Ibatis框架实现这一功能。如果按照一般的编程方式,我们会写一个sql,然后调用QueryForList方法得到帐户的List,然后遍历List逐条对数据进行结息操作,但这样做很可能会出现性能问题,如:
    1.对JVM内存的大量消耗;
    2.大量对象的密集创建和销毁对GC带来很大的负担;
出现性能问题的原因在于:QueryForList是把查询结果构造完成以后才交给结息程序的,这意味着JVM会爆发性的创建一千万个对象到内存中,并且这1千万个对象会在内存中持续很长时间——因为只有等所有帐户结完息之后这些对象才会失去引用,那么可以推断这些对象也是爆发式的销毁,这对内存的消耗是非常巨大的。

为应对这样的问题,Ibatis提供了RowHandler接口,允许程序员对查询结果进行自定义的处理,RowHandler接口代码如下:

/**
 * Event handler for row by row processing.
 * <p/>
 * The RowHandler interface is used by the SqlMapSession.queryWithRowHandler() method.
 * Generally a RowHandler implementation will perform some row-by-row processing logic
 * in cases where there are too many rows to efficiently load into memory.
 * <p/>
 * Example:
 * <pre>
 * sqlMap.queryWithRowHandler ("findAllEmployees", null, new MyRowHandler()));
 * </pre>
 */
public interface RowHandler {

  /**
   * Handles a single row of a result set.
   * <p/>
   * This method will be called for each row in a result set.  For each row the result map
   * will be applied to build the value object, which is then passed in as the valueObject
   * parameter.
   *
   * @param valueObject The object representing a single row from the query.
   * @see com.ibatis.sqlmap.client.SqlMapSession
   */
  void handleRow(Object valueObject);

}

DefaultRowHandler是RowHandler接口的默认实现,代码如下:

public class DefaultRowHandler implements RowHandler {

  private List list = new ArrayList();

  public void handleRow(Object valueObject) {
    list.add(valueObject);
  }

  public List getList() {
    return list;
  }

  public void setList(List list) {
    this.list = list;
  }

}

我们调用queryForList方法得到的list便是由该接口提供的,Ibatis构造这个list的过程大致如下:当执行完查询sql得到ResultSet之后,会循环遍历这个ResultSet,在每一次循环中调用DefaultRowHandler的handleRow方法,循环结束list也就构造完成了,然后把list返回。到这里相信大家已经知道RowHandler的本质了,我们只需要定义自己的Handler,然后调用queryWithRowHandler方法,Ibatis执行的就是我们自定义的handleRow方法了,在handleRow方法里把结息操作做了,就不会出现内存暴增的情况了,因为每次循环结束valueObject便失去了句柄引用,很快就会被GC清理掉了。

上面通过自定义RowHandler解决了内存占用的问题,但还有其他问题,handleRow中的结息操作是同步执行还是异步执行呢?如果是同步执行,那可能会带来新的问题:
    首先,同步执行并没有减少总时间,用queryForList和用queryWithRowHander的总时间都为——(数据库端的查询时间+Java端取数据的时间+10000000*结息时间);
    再有,ResultSet并不是一次性把1千万条数据返回的,而是在循环遍历的过程中随用随取,比如先取10条,执行到第11次循环的时候再取10条,依此类推,而一次取多少和FetchSize的设置有关系,那么在1千万个账户结息完成之前数据库连接都无法释放并且数据库端会维持一个Cursor配合ResultSet取数据,这会带来长时间的、很大的资源开销。
    那么,将结息息操作异步化则是更好的方案,我们可以在handlRow方法中把账户对象Add到另一个或多个线程的结息队列中,那么取数据和结息便能并行执行,这样总时间会少一些(FetchSize不变的情况下缩短的是结息的时间,数据库端的查询时间和Java端取数据的时间是不变的),但可能不会少太多,这和FethSeize的设置有关系。如果FetchSize比较大,[Java端取数据的时间]会很短,那并行的时间也会很短,则短时间内完成结息的账户数量也不会太多;如果FetchSize比较小,那取数据的时间会较长,并行的时间也会长,则完成结息的账户数量也会多一些。
    大家应该已经看到了,其实异步化之后,如果FetchSize很大的话,queryWithRowHandler和queryForList就没什么区别了,因为数据很快就能取完(我做测试:两千六百多万条记录3分多钟就取完了),同样会带来内存的暴增,而FetchSize太小的话又会导致Java和数据库之间频繁的网络IO,一样影响性能。所以FetchSize的设置就很关键,太大太小都不行,需要考虑[取数据]的速率、[结息操作]的速率和内存大小三方面的要素。

上面说了这么多,都是偏理论性的东西,一切都需根据自己的实际情况进行抉择。
     * 如果内存够大,queryForList一样很OK;
     * 如果内存是瓶颈,可以改用queryWithRowHandler,如果连接池不紧张,数据库端没什么资源压力,同步handle一样OK;
     * 如果还有优化的需求,则可以考虑异步handle,但此时FetchSize的设置需要认真权衡,即不能太大也不能太小。

猜你喜欢

转载自blog.csdn.net/lubiaopan/article/details/40299507