如果一个场景:账户表中有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设置需要仔细权衡,这不是太大,也不能太小。
版权声明:本文博主原创文章,博客,未经同意不得转载。