ORA-01000,maximum-open-cursors 错误,是 Oracle 数据库开发中极为常见的错误。在 Java 的上下文中,当应用程序尝试打开的 ResultSet 多于数据库实例上配置的游标时,就会发生这种情况。
常见原因有:
配置错误
- 您的应用程序中查询数据库的线程比数据库上的游标多。一种情况是您的连接和线程池大于数据库上的游标数。
- 您有许多开发人员或应用程序连接到同一个数据库实例(可能包括许多模式),并且您一起使用了太多的连接。
解决方案:
- 增加数据库上的游标数量(如果资源允许)或
- 减少应用程序中的线程数。
光标泄漏
- 应用程序没有关闭 ResultSets(在 JDBC 中)或游标(在数据库上的存储过程中)
- 解决方案:光标泄漏是错误;增加数据库上的游标数量只会延迟不可避免的故障。可以使用静态代码分析、JDBC或应用程序级日志记录和数据库监控来发现泄漏。
背景
本节介绍游标背后的一些理论以及应如何使用 JDBC。如果你不需要知道背景,你可以跳过这一步,直接进入“消除泄漏”。
什么是游标?
游标是数据库上保存查询状态的资源,特别是读取器在 ResultSet 中的位置。每个 SELECT 语句都有一个游标,PL/SQL 存储过程可以根据需要打开和使用任意数量的游标。您可以在Orafaq上找到有关游标的更多信息。
一个数据库实例通常服务于几个不同的模式,许多不同的用户每个都有多个会话。为此,它为所有模式、用户和会话提供了固定数量的游标。当所有游标都打开(正在使用)并且请求进入需要新游标时,请求将失败并出现 ORA-010000 错误。
查找和设置游标的数量
该编号通常由 DBA 在安装时配置。当前使用的游标数量、最大数量和配置可以在Oracle SQL Developer的管理员功能中访问。从 SQL 可以设置:
ALTER SYSTEM SET OPEN_CURSORS=1337 SID='*' SCOPE=BOTH;
将 JVM 中的 JDBC 与 DB 上的游标相关联
下面的 JDBC 对象与以下数据库概念紧密耦合:
- JDBC连接是数据库会话的客户端表示并提供数据库事务。一个连接在任何时候只能打开一个事务(但事务可以嵌套)
- JDBC ResultSet由数据库上的单个游标支持。当在 ResultSet 上调用 close() 时,将释放光标。
- JDBC CallableStatement调用数据库上的存储过程,通常用 PL/SQL 编写。存储过程可以创建零个或多个游标,并且可以将游标作为 JDBC ResultSet 返回。
JDBC 是线程安全的:在线程之间传递各种 JDBC 对象是完全可以的。
例如,您可以在一个线程中创建连接;另一个线程可以使用此连接来创建 PreparedStatement,而第三个线程可以处理结果集。一个主要的限制是您在任何时候都不能在一个 PreparedStatement 上打开多个 ResultSet。请参阅Oracle DB 是否支持每个连接的多个(并行)操作?
请注意,数据库提交发生在连接上,因此该连接上的所有 DML(插入、更新和删除)将一起提交。因此,如果要同时支持多个事务,则每个并发 Transaction 必须至少有一个 Connection。
关闭 JDBC 对象
执行 ResultSet 的典型示例是:
Statement stmt = conn.createStatement();
try {
ResultSet rs = stmt.executeQuery( "SELECT FULL_NAME FROM EMP" );
try {
while ( rs.next() ) {
System.out.println( "Name: " + rs.getString("FULL_NAME") );
}
} finally {
try { rs.close(); } catch (Exception ignore) { }
}
} finally {
try { stmt.close(); } catch (Exception ignore) { }
}
请注意 finally 子句如何忽略 close() 引发的任何异常:
- 如果您只是在没有 try {} catch {} 的情况下关闭 ResultSet,它可能会失败并阻止 Statement 被关闭
- 我们希望允许在尝试的主体中引发的任何异常传播给调用者。如果您有一个循环,例如创建和执行语句,请记住关闭循环中的每个语句。
在 Java 7 中,Oracle 引入了AutoCloseable 接口,它用一些不错的语法糖替换了大部分 Java 6 样板。
持有 JDBC 对象
JDBC 对象可以安全地保存在局部变量、对象实例和类成员中。通常更好的做法是:
- 使用对象实例或类成员来保存在较长时间内多次重复使用的 JDBC 对象,例如 Connections 和 PreparedStatements
- 为 ResultSet 使用局部变量,因为这些变量通常是在单个函数的范围内获得、循环然后关闭的。
但是,有一个例外:如果您使用 EJB 或 Servlet/JSP 容器,则必须遵循严格的线程模型:
- 只有应用服务器创建线程(处理传入请求的线程)
- 只有应用程序服务器创建连接(您从连接池中获取)
- 在调用之间保存值(状态)时,您必须非常小心。永远不要将值存储在您自己的缓存或静态成员中——这在集群和其他奇怪的情况下是不安全的,并且应用程序服务器可能会对您的数据做可怕的事情。而是使用有状态 bean 或数据库。
- 特别是,永远不要在不同的远程调用上保存 JDBC 对象(Connections、ResultSets、PreparedStatements 等)——让应用程序服务器来管理它。Application Server 不仅提供了一个连接池,它还缓存了您的 PreparedStatements。
消除泄漏
有许多流程和工具可用于帮助检测和消除 JDBC 泄漏:
在开发过程中 - 尽早发现错误是迄今为止最好的方法:
开发实践:良好的开发实践应该在软件离开开发人员办公桌之前减少软件中的错误数量。具体做法包括:
- 结对编程,教育没有足够经验的人
- 代码审查,因为多只眼睛胜过一只眼睛
- 单元测试,这意味着您可以从测试工具中运行任何和所有代码库,这使得重现泄漏变得微不足道
- 使用现有库进行连接池,而不是构建自己的库
静态代码分析:使用像优秀的Findbugs这样的工具来执行静态代码分析。这会发现许多未正确处理 close() 的地方。Findbugs 有一个 Eclipse 插件,但它也可以一次性独立运行,集成到 Jenkins CI 和其他构建工具中
在运行时:
可持有性和提交
- 如果 ResultSet 可保持性为 ResultSet.CLOSE_CURSORS_OVER_COMMIT,则在调用 Connection.commit() 方法时将关闭 ResultSet。这可以使用 Connection.setHoldability() 或使用重载的 Connection.createStatement() 方法来设置。
在运行时记录。
- 在您的代码中放置好的日志语句。这些内容应该清晰易懂,以便客户、支持人员和队友无需培训即可理解。它们应该简洁,包括打印关键变量和属性的状态/内部值,以便您可以跟踪处理逻辑。良好的日志记录是调试应用程序的基础,尤其是那些已部署的应用程序。
您可以将调试 JDBC 驱动程序添加到您的项目中(用于调试 - 不要实际部署它)。一个例子(我没有用过)是log4jdbc。然后你需要对这个文件做一些简单的分析,看看哪些执行没有相应的关闭。如果存在潜在问题,则应突出计算打开和关闭
- 监控数据库。使用 SQL Developer 的“监控 SQL”功能或Quest 的 TOAD等工具监控正在运行的应用程序。本文介绍了监控。在监视期间,您查询打开的游标(例如从表 v$sesstat)并查看它们的 SQL。如果游标的数量在增加,并且(最重要的是)被一个相同的 SQL 语句支配,那么您就知道该 SQL 存在泄漏。搜索您的代码并查看。
其他想法
你可以使用 WeakReferences 来处理关闭连接吗?
弱引用和软引用是允许您以允许 JVM 在其认为合适的任何时间对所指对象进行垃圾收集的方式引用对象的方式(假设该对象没有强引用链)。
如果将构造函数中的 ReferenceQueue 传递给软引用或弱引用,则当对象发生时(如果它完全发生),当对象被 GC'ed 时,该对象被放置在 ReferenceQueue 中。使用这种方法,您可以与对象的最终确定进行交互,并且您可以在那一刻关闭或最终确定对象。
幻影引用有点奇怪。它们的目的只是控制最终确定,但您永远无法获得对原始对象的引用,因此很难在其上调用 close() 方法。
但是,尝试控制 GC 何时运行并不是一个好主意(Weak、Soft 和 PhantomReferences会在对象已排队等待 GC之后让您知道)。事实上,如果 JVM 中的内存量很大(例如 -Xmx2000m),您可能永远不会GC 对象,并且您仍然会遇到 ORA-01000。如果 JVM 内存相对于您的程序要求来说很小,您可能会发现 ResultSet 和 PreparedStatement 对象在创建后立即被 GC(在您可以读取它们之前),这可能会使您的程序失败。
TL;DR:弱引用机制不是管理和关闭 Statement 和 ResultSet 对象的好方法。