java.sql.SQLException: - ORA-01000: 超出最大打开游标数

2022-08-31 08:32:28

我收到一个 ORA-01000 SQL 异常。所以我有一些与之相关的疑问。

  1. 最大打开游标数是否与 JDBC 连接数完全相关,或者它们是否也与我们为单个连接创建的语句和结果集对象相关?(我们正在使用连接池)
  2. 有没有办法配置数据库中语句/结果集对象的数量(如连接)?
  3. 在单线程环境中,是否建议使用实例变量语句/结果集对象而不是方法局部语句/结果集对象?
  4. 在循环中执行预准备语句是否会导致此问题?(当然,我可以使用sqlBatch)注意:一旦循环结束,pStmt就会关闭。

    { //method try starts  
      String sql = "INSERT into TblName (col1, col2) VALUES(?, ?)";
      pStmt = obj.getConnection().prepareStatement(sql);
      pStmt.setLong(1, subscriberID);
      for (String language : additionalLangs) {
        pStmt.setInt(2, Integer.parseInt(language));
        pStmt.execute();
      }
    } //method/try ends
    
    { //finally starts
       pStmt.close()
    } //finally ends 
    
  5. 如果在单个连接对象上多次调用 conn.createStatement() 和 conn.prepareStatement(sql) 会发生什么情况?

编辑1: 6. 使用弱/软引用语句对象是否有助于防止泄漏?

编辑2: 1. 有没有办法,我可以在我的项目中找到所有缺少的“statement.close()”?我知道这不是内存泄漏。但是我需要找到一个符合垃圾回收条件的语句引用(其中不执行 close() ) ?任何可用的工具?还是我必须手动分析它?

请帮助我理解它。

溶液

在 Oracle DB 中查找用户名 -VELU 的打开游标

转到 ORACLE machine 并启动 sqlplus 作为 sysdba。

[oracle@db01 ~]$ sqlplus / as sysdba 

然后运行

SELECT   A.VALUE,
    S.USERNAME,
    S.SID,
    S.SERIAL#
  FROM V$SESSTAT A,
    V$STATNAME B,
    V$SESSION S
  WHERE A.STATISTIC# = B.STATISTIC#
    AND S.SID        = A.SID
    AND B.NAME       = 'opened cursors current'
    AND USERNAME     = 'VELU';

如果可能的话,请阅读我的答案,以更好地了解我的解决方案


答案 1

ORA-01000(最大打开游标错误)是 Oracle 数据库开发中极其常见的错误。在 Java 上下文中,当应用程序尝试打开的结果集数多于数据库实例上配置的游标数时,就会发生这种情况。

常见原因是:

  1. 配置错误

    • 应用程序中查询数据库的线程数多于数据库上的游标数。一种情况是,您的连接和线程池大于数据库上的游标数。
    • 您有许多开发人员或应用程序连接到同一数据库实例 (可能包含许多架构),并且您总共使用了太多连接。
    • 溶液:

  2. 游标泄漏

    • 应用程序未关闭 ResultSet(在 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 上调用 close() 时,将释放游标。
  • JDBC CallableStatement 调用数据库上的存储过程,通常用 PL/SQL 编写。该存储过程可以创建零个或多个游标,并且可以将游标作为 JDBC 结果集返回。

JDBC 是线程安全的:在线程之间传递各种 JDBC 对象是完全可以的。

例如,您可以在一个线程中创建连接;另一个线程可以使用此连接创建 ReadyStatement,第三个线程可以处理结果集。唯一的主要限制是,在任何时候都不能在单个预准备语句上打开多个 ResultSet。请参阅 Oracle 数据库是否支持每个连接执行多个(并行)操作?

请注意,数据库提交发生在连接上,因此该连接上的所有 DML(INSERT、UPDATE 和 DELETE)将一起提交。因此,如果要同时支持多个事务,则必须为每个并发事务至少具有一个连接。

关闭 JDBC 对象

执行结果集的典型示例如下:

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() 引发的任何异常:

  • 如果只是关闭 ResultSet 而不尝试 {} 捕获 {},则它可能会失败并阻止语句被关闭
  • 我们希望允许在 try 的正文中引发的任何异常传播到调用方。如果您有一个循环,例如,创建和执行语句,请记住关闭循环中的每个语句。

在Java 7中,Oracle引入了AutoCloseable接口,该接口用一些不错的语法糖取代了Java 6的大部分样板。

持有 JDBC 对象

JDBC 对象可以安全地保存在局部变量、对象实例和类成员中。通常,更好的做法是:

  • 使用对象实例或类成员来保存在较长时间内多次重复使用的 JDBC 对象,例如连接和准备语句
  • 对 ResultSet 使用局部变量,因为这些变量通常在单个函数的范围内获取、循环然后关闭。

但是,有一个例外:如果您使用的是 EJB 或 Servlet/JSP 容器,则必须遵循严格的线程模型:

  • 只有应用程序服务器创建线程(用于处理传入请求)
  • 只有应用程序服务器创建连接(从连接池中获取连接)
  • 在两次调用之间保存值(状态)时,必须非常小心。切勿将值存储在您自己的缓存或静态成员中 - 这在集群和其他奇怪情况下是不安全的,应用程序服务器可能会对您的数据造成可怕的影响。请改用有状态 Bean 或数据库。
  • 特别是,永远不要在不同的远程调用上保存 JDBC 对象(连接、结果集、准备语句等)-让应用程序服务器来管理它。应用程序服务器不仅提供连接池,还缓存您的预准备语句。

消除泄漏

有许多流程和工具可用于帮助检测和消除 JDBC 泄漏:

  1. 在开发过程中 - 尽早捕获错误是迄今为止最好的方法:

    1. 开发实践:良好的开发实践应该在软件离开开发人员办公桌之前减少软件中的错误数量。具体做法包括:

      1. 结对编程,教育那些没有足够经验的人
      2. 代码审查,因为多只眼睛比一只眼睛好
      3. 单元测试,这意味着您可以从测试工具中执行任何和所有代码库,这使得重现泄漏变得微不足道
      4. 使用现有库进行连接池,而不是构建自己的库
    2. 静态代码分析:使用像优秀的Findbugs这样的工具来执行静态代码分析。这会拾取许多未正确处理 close() 的位置。Findbugs有一个Eclipse插件,但它也可以独立运行一次性,并集成到Jenkins CI和其他构建工具中

  2. 在运行时:

    1. 可保持性和提交

      1. 如果结果集可保持性ResultSet.CLOSE_CURSORS_OVER_COMMIT,则在调用 Connection.commit() 方法时将关闭结果集。这可以使用 Connection.setHoldability() 或使用重载的 Connection.createStatement() 方法进行设置。
    2. 运行时日志记录。

      1. 在代码中放置良好的日志语句。这些应该是清晰易懂的,以便客户,支持人员和队友无需培训即可理解。它们应该简洁明了,包括打印关键变量和属性的状态/内部值,以便您可以跟踪处理逻辑。良好的日志记录是调试应用程序的基础,尤其是那些已部署的应用程序。
      2. 您可以将调试 JDBC 驱动程序添加到项目中(用于调试 - 实际上并不部署它)。一个例子(我没有用过它)是log4jdbc。然后,您需要对此文件进行一些简单的分析,以查看哪些执行没有相应的关闭。如果存在潜在问题,则计算打开和关闭次数应突出显示

        1. 监视数据库。使用 SQL Developer 'Monitor SQL' 函数或 Quest 的 TOAD 等工具监控正在运行的应用程序。本文介绍了监视。在监视期间,查询打开的游标(例如,来自表 v$sesstat)并查看其 SQL。如果游标的数量在增加,并且(最重要的是)被一个相同的 SQL 语句所控制,那么您就知道该 SQL 存在泄漏。搜索代码并查看。

其他想法

是否可以使用弱引用来处理关闭连接?

弱引用和软引用是允许您以一种允许 JVM 在它认为合适的任何时间对引用对象进行垃圾回收的方式引用的方法(假设该对象没有强引用链)。

如果将构造函数中的 ReferenceQueue 传递给软引用或弱引用,则当对象在发生时(如果它发生时)被 GC 化时,该对象将被放置在 ReferenceQueue 中。使用此方法,您可以与对象的定版进行交互,并且可以在该时刻关闭或完成对象。

幻像引用有点奇怪;它们的目的只是为了控制终结,但你永远无法获得对原始对象的引用,所以很难在其上调用close()方法。

但是,尝试控制 GC 的运行时间很少是一个好主意(弱引用、软引用和幻像引用在对象排队为 GC 之后通知您)。事实上,如果JVM中的内存量很大(例如-Xmx2000m),你可能永远不会GC对象,你仍然会遇到ORA-01000。如果 JVM 内存相对于程序的要求很小,您可能会发现 ResultSet 和 PreparedStatement 对象在创建后立即被 GCed(在您可以从中读取之前),这可能会使程序失败。

TL;DR:弱引用机制不是管理和关闭语句和 ResultSet 对象的好方法。


答案 2

我增加了一些理解。

  1. Cursor 只是关于一个语句 objecct;它既不是 resultSet 也不是连接对象。
  2. 但是我们仍然必须关闭结果集以释放一些预言机内存。不过,如果您不关闭结果集,则不会将其计入 CURSORS。
  3. 结束语句对象也会自动关闭结果集对象。
  4. 将为所有 SELECT/INSERT/UPDATE/DELETE 语句创建游标。
  5. 每个 ORACLE 数据库实例都可以使用 oracle SID 进行标识;同样,ORACLE DB 可以使用连接 SID 来标识每个连接。这两个 SID 是不同的。
  6. 所以ORACLE会话只不过是一个jdbc(tcp)连接;这只不过是一个 SID。
  7. 如果我们将最大游标设置为 500,则它仅适用于一个 JDBC 会话/连接/SID。
  8. 因此,我们可以有许多JDBC连接及其各自的游标(语句)数量。
  9. 一旦JVM被终止,所有的连接/游标都将关闭,或者JDBCConnection被关闭 相对于该连接的游标将关闭。

Loggin as sysdba.

在Putty(Oracle login)中:

  [oracle@db01 ~]$ sqlplus / as sysdba

在 SqlPlus 中:

用户名:sys as sysdba

将session_cached_cursors值设置为 0,使其不会关闭游标。

 alter session set session_cached_cursors=0
 select * from V$PARAMETER where name='session_cached_cursors'

选择 DB 中每个连接的现有OPEN_CURSORS值集

 SELECT max(a.value) as highest_open_cur, p.value as max_open_cur FROM v$sesstat a, v$statname b, v$parameter p WHERE a.statistic# = b.statistic# AND b.name = 'opened cursors current' AND p.name= 'open_cursors'  GROUP BY p.value;

下面是用于查找具有打开游标值的 SID/连接列表的查询。

 SELECT a.value, s.username, s.sid, s.serial#
 FROM v$sesstat a, v$statname b, v$session s
 WHERE a.statistic# = b.statistic#  AND s.sid=a.sid 
 AND b.name = 'opened cursors current' AND username = 'SCHEMA_NAME_IN_CAPS'

使用以下查询在打开的游标中标识 sql

 SELECT oc.sql_text, s.sid 
 FROM v$open_cursor oc, v$session s
 WHERE OC.sid = S.sid
 AND s.sid=1604
 AND OC.USER_NAME ='SCHEMA_NAME_IN_CAPS'

现在调试代码并享受!!!:)


推荐