关于ORA-00054异常的一种数据库内因

声明:受作者本人水平所限,文中难免有谬误,敬请指摘!

----------------------------------------------------

【现象】

当某个数据库用户在数据库中插入、更新、删除一个表的数据,或者增加一个表的主键时或者表的索引时,有时会出现ora-00054:resource busy and acquire with nowait specified这样的错误。

 

【原因】

主要有几大类原因,这里主要介绍其中的两大类原因(编写应用代码的程序员比较关心的):

一、操作类原因(外因)。

1、执行DML时,当要操作的数据已经被加锁,这时在另一个回话中再次要取得这个对象的锁时,新会话要么被挂起,要么抛出ORA-00054异常。

2、当DML在执行中,又同时在相同对象上执行DDL语句。比如Update操作的事务尚未提交,在另一个会话中开始执行表结构修改、变更索引的SQL时,也往往会出现ORA-00054异常。

 

举一个纯粹的DML操作出现ORA-00054异常的例子:

-- session 1

SQL> select * from zjh_6 where id=1 for update nowait;

 

        ID

----------

                    1

 

-- session 2

SQL> select * from zjh_6 where id=1 for update nowait;

select * from zjh_6 where id=1 for update nowait

              *

第 1 行出现错误:

ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效

 

这是比较常见的一种现象,因为id=1的记录在session 1中已经被锁定,那么在session 2中再次要对这条记录加锁时,就抛出了异常。

 

二、数据库原因(内因)。

跟前面的现象稍有区别,前面是在不同的会话中对相同的记录获取锁时出现了资源被占用的情况,还有一种情况是,在不同的会话中对不同的记录获取锁时也出现了ORA-00054错误。

 

一般在网络上出现的说明大多都是针对操作类原因的说明,本文要强调的则是数据库方面的内因。

 

举个例子来说明,在库上按照以下格式建了一个表:

-- Create table

create table T1

(

  id   NUMBER(3) not null,

  name NVARCHAR2(20) not null

)

tablespace DATA01

  pctfree 10

  initrans 1

  maxtrans 255

  storage

  (

    initial 64K

    minextents 1

    maxextents unlimited

  );

 

-- Create/Recreate primary, unique and foreign key constraints

alter table T1

  add constraint PK_T1 primary key (ID)

  using index

  tablespace INDEX01

  pctfree 10

  initrans 2

  maxtrans 255;

 

-- 插入数据

declare id number(3);

begin

  for id in 1..300 loop

    insert into t1(id, name) values(id, lpad('1',20,'1'));

  end loop;

end;

/

commit;

 

 

执行下面的Java main函数:

Main.java

public class Main {

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 200; i++) {

            Runnable run = new Oper(i);

            Thread t = new Thread(run);

            t.start();

            Thread.sleep(1);

            System.out.println("Started, id = " + i);

        }

 

        Thread.sleep(800000);

    }

}

 

Oper.java

import java.sql.Connection;

import java.sql.PreparedStatement;

 

public class Oper implements Runnable {

    private int id;

 

    public Oper(int id) {

        this.id = id;

    }

 

    @Override

    public void run() {

        Connection conn = null;

        PreparedStatement pre = null;

        try {

            conn = DbUtil.getConn();

 

            String sql = "select * from t2 where id=? for update nowait";

            pre = conn.prepareStatement(sql);// 实例化预编译语句

 

            pre.setInt(1, this.id);

            pre.executeUpdate();

 

            System.out.println("OK, id = " + this.id);

 

            Thread.sleep(800000);

        }

        catch (Exception e) {

            System.out.println("Err, id = " + this.id + ": " + e);

        }

        finally {

            DbUtil.close(pre, conn);

        }

    }

}

 

DbUtil.java

package com.zjh;

 

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.SQLException;

 

public class DbUtil {

 

public static Connection getConn() throws ClassNotFoundException, SQLException {

Connection conn = null;

Class.forName("oracle.jdbc.driver.OracleDriver");

String url = "jdbc:oracle:thin:@//192.168.1.109:1521/PDBCC";

String UserName = "c##cc";

String Password = "123456";

conn = DriverManager.getConnection(url, UserName, Password);

conn.setAutoCommit(false);

return conn;

}

 

    public static void close(PreparedStatement pre, Connection conn) {

        try {

            if (pre != null)

                pre.close();

            if (conn != null)

                conn.close();

        }

        catch (SQLException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        }

    }

 

}        

 

【运行结果】

Started, id = 1

Started, id = 2

… 此处省略

Started, id = 199

Started, id = 200

OK, id = 19

OK, id = 8

OK, id = 9

OK, id = 34

… 此处省略

OK, id = 13

OK, id = 5

Err, id = 29: java.sql.SQLException: ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效

Err, id = 152: java.sql.SQLException: ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效

… 此处省略

Err, id = 137: java.sql.SQLException: ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效

 

【分析】

代码中对不同id的记录进行了select … for update nowait操作,按常理说好像不应该报这个错。但实际上这个跟表T1的表结构和几个参数相关:

1)行记录长度(各个字段定义的长度的和)

2PCTFREE参数

3INITRANS参数

 

解释如下:

1)行记录长度越短,那么每个Block中存储的记录数越多,导致并发状态下select … for update nowait操作的行记录处于同一个Block块中的几率越大。

2select … for update涉及到数据库事务,每个block在头部会有一个事务槽,每行记录参与事务时都会占用槽的一个位置。事务槽的大小取决于PCTFREE参数,这个参数值越大,那么可供使用的事务槽的空间越大。如果PCTFREE值越小,那么就留事务槽的空间就越小。

3)如果行记录的大小比较小,而且PCTFREE也比较小,那么在对同一个Block中的多条记录进行事务操作时,极有可能出现事务槽不够用,本例的错误就是这个原因导致。

4INITRANS就是一个Block中事务槽的默认大小,对于Table而言这个参数的默认值一般为1,最大值为255Oracle 10g之后,最大值已经没有意义)。默认值只是表示Block初始化时预留的空间,实际在运行过程中如果Block中事务比较多,那么事务槽会动态的增长(受PCTFREE所限)。如果默认值比较大,Block中可供用来存储数据的空间就会变少(实际上就是Block中存储的记录数减少,降低了Block中的事务并发量),一般而言INITRANS范围在1-4就够用了。如果INITRANS的值变大,那么一个Block中可支持的并发事务也会增大。

 

为了证明以上结论,再做一个实验。

执行以下脚本:

-- Create table

create table T2

(

  id   NUMBER(3) not null,

  name NVARCHAR2(2000) not null

)

tablespace DATA01

  pctfree 10

  initrans 1

  maxtrans 255

  storage

  (

    initial 64K

    minextents 1

    maxextents unlimited

  );

 

-- Create/Recreate primary, unique and foreign key constraints

alter table T2

  add constraint PK_T2 primary key (ID)

  using index

  tablespace INDEX01

  pctfree 10

  initrans 2

  maxtrans 255;

 

-- 插入数据

declare id number(3);

begin

  for id in 1..300 loop

    insert into t2(id, name) values(id, lpad('1',2000,'1'));

  end loop;

end;

/

 

commit;

 

然后再次执行前文的Java代码,注意把代码中的表名改成T2,观察输出结果:

Started, id = 1

Started, id = 2

… 此处省略

Started, id = 200

OK, id = 117

OK, id = 80

… 此处省略

OK, id = 191

OK, id = 136

可以发现,这次没有出现ORA-00054错误。

 

【分析】

因为T2的行记录长度比较大,一个Block只能存储1-2条记录,那么每个Block的事务并发量最大也就是1-2而已,这时对于事务槽空间大小的要求就很低了,故不会出现资源不够的情况,所以不报错能够正常运行。

 

【结论】

好了,问题来了,遇到ORA-00054错误怎么办?

答:看情况。

如果是因为针对同一个记录出现的ORA-00054错误,那么肯定是SQL语句的问题,需要修改代码来解决。

如果是后一种情况,那么怎么办?我的答案是一般情况下不需要考虑,在真实的业务场景中,插入Block块中的记录未必是主键连续的,而且一般也不会针对主键连续的一段记录进行批量操作(而且是在多个数据库会话中)。这种情况一般出现在对系统进行压力测试的情形中,如果非要进行类似的压力测试,但又想避开ORA-00054错误,那么有两个可选办法:

1)在生成测试数据时,不要按照主键顺序插入数据,把数据打散插入,比如可以采用反向索引,让记录分散在不同的Block中。不过任何事情都有相反的一面,反向索引在进行范围查找时效率比较低,关于反向索引的具体信息请自行查阅相关材料。

2)压力测试选取数据时,不要使用连续的记录,把选择数据也打散,避免数据集中在少数Block上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值