ORACLE数据库超出游标异常解决(maximum open cursors exceeded)
问题背景
在项目上线实际运行中,有某处功能涉及到批量增加的实现。之前客户在实际使用中每50条增加一次,系统运行无误,后该处需求增加,意图每次增加1000条,遂遇到此错误。
处理难点
实际项目是一个很大的共享项目,客户有自己的云平台,我方项目是部署在该云平台上的,以及数据库也是客户自有数据库。生产,UAT,FAT,DEV都无法找到日志进行定位。本地进行部署时又无法复现该错误,究其原因,我在本地查询ORACLE的游标数只有200左右(系统未使用时),而在UAT,未进行插入就已有5000-6000的游标。因此本地插入1000条数据只是时间问题,并不会报超出游标。
处理思路
- 治标不治本的方法,加大最大游标数,在本案例中并不适用,无法连接到客户数据库。
- 通过从网上查阅资料得知,超出游标的最大可能性为:Statement或PreparedStatement放在循环中使用,循环中也未对资源进行关闭。我这里是插入情况,暂不考虑结果集。检查方法这两个资源是否放在循环中使用了并未关闭。
- 本地无法复现,那么就实时观察游标占用情况,语句如下:
select count(*) from v$open_cursor;
- 在对第二种进行尝试无果之后,进一步思考有无其他解决方法。后决定对数据进行分片处理,给每一片数据提供一个新的连接而不是Statement。
处理结果
第二种情况:
通过查看本地代码得知,之前代码写法有一个存在隐患的地方。
for(i=0;i<list.size();i++){
....
pre.addBatch();
}
pre.executeBatch();
pre.clearBatch();
对结果集的操作是通过addBatch()方法先进行预编译,在循环之外在去执行并清理资源。遂对其进行改动。
for(i=0;i<list.size();i++){
....
//每50条执行一次
if ((i+1)%50 == 0){
pre.executeBatch();
pre.clearBatch();
}else if ((list.size()-1) == i){
pre.executeBatch();
pre.clearBatch();
}
}
每50条数据执行一次插入并清除Batch的缓存。
很遗憾,这种处理方式是无效的,添加500条,游标最高能达到740.
遂又对代码进行查看,发现每一处该关的资源全部都是关闭了的,实在是找不出问题出现在哪里。身边同事对Oracle不甚熟悉,问题僵持在这里。
进阶方案:
对数据进行分片,分片方法网上有很多,自己分组也可以。这里用谷歌的工具包:
//50条数据一组
List<List<T>> partition =Lists.partition(rowList, 50);
之后给每一组数据提供新的连接,该组数据处理完之后先关闭Statement,再关闭连接即可。
List<List<T>> partition = Lists.partition(rowSetList, 50);
for (List<T> rowSets : partition) {
// 这里的Connection是自己的工具类,提供一个新的连接即可
Connection conn = Connection.create();
pre = conn.prepareStatement(sql.toString());
for (RowSet rowSet : rowSets) {
······
pre.addBatch();
}
pre.executeBatch();
pre.clearBatch();
//这俩关资源也是自己的工具类,能正常关闭资源即可
JDBResource.release(pre);
Connection.closeJConnection(conn);
}
对代码进行如上改动之后,再观察插入500条数据的游标占用,始终保持在190-250之间(偶有离群点,300-400之间),问题得以解决。
其他
- 对于有离群点的情况,需要等改动升级到UAT环境,进行实际测试,如果仍报错,那么说明有可能是离群点造成的,这时候只需要进一步改小每一片数据的容量即可,降低阈值。
- 如果有连接池的使用,必须要先关掉Statement的资源再关闭连接,否则连接始终存在占用。