问题综述
1
这段时间应用一直被一个诡异的 ORA-00060 的错误所困扰,众所周知,造成 ORA-00060 的原因是由于应用逻辑,而非 Oracle 数据库自己,之所以说诡异(“诡异”可能不准确,只能说这种场景,以前碰见的少,并未刻意关注),是因为这次不是常见的,由于读取数据顺序有交叉,导致ORA-00060,关于读取数据顺序错误,导致ORA-00060的错误场景,可以参考《ORA-00060的示例与若干场景》(https://blog.csdn.net/bisal/article/details/14227997)。
问题引入
2
再说我碰见的问题之前,引用一些 JL 大神《Oracle Core》,对于死锁的一些介绍,一些 (易混淆/不清晰) 概念,后面可能会用上。
Oracle 用户群里一个众所周知的说法是“读不阻塞写,写不阻塞读”,但这仅限于数据级别;当深入到底层的原始内存级别时,有时候读者必须阻塞写者才行,并且单个写操作必须阻塞其他所有操作。
每个由事务修改的数据(或索引)块都会在他的事务列表中引用该事务表槽。执行事务的会话都会为事务表槽创建一个排队资源(类型为 TX,id1 表示 undo 段号和槽号,id2 表示槽序列号)来锁定他,并将一个排队( enqueue )附加到这个资源上-具体来说,是 x$ktcxb 中的一行,而不是 x$ksqeq- 锁模式为 6(独占锁)。
混淆:
- 当然可能有多个会话陷入循环等待,死锁不仅仅限于两个会话之间,尽管最常见的是两个。
- 尽管模式6类型的TX锁是死锁中最常见的,但任何时候只要等待一个锁,都有可能会出现ORA-00060场景。
死锁场景:
- 两个会话试图插入相同的主键值。
- 一个会话在另一个会话删除父行时插入一行到子表中。
- 一个会话插入一条父行,然后另一个会话在父行提交前插入子行。
- 两个会话试图删除由同一个位图索引块(chunk)所覆盖的行。
本质上讲,如果由于索引(或由这些索引关联的约束)争用,导致你必须等待其他会话提交,那么你会看到一个模式 4 的 TX 等待。甚至有一个不是由编码问题引起的索引情况-当你等待其他会话完成索引块分裂时。
如下是官方文档,对于 TX 锁的介绍,字面含义“行锁”(“行锁”是否正确?后面 JL 帖子回复,会纠正这个错误),
Row Locks (TX)
A row lock, also called a TX lock, is a lock on a single row of table. A transaction acquires a row lock for each row modified by an INSERT, UPDATE,DELETE, MERGE, or SELECT ... FOR UPDATE statement. The row lock exists until the transaction commits or rolls back.
Row locks primarily serve as a queuing mechanism to prevent two transactions from modifying the same row. The database always locks a modified row in exclusive mode so that other transactions cannot modify the row until the transaction holding the lock commits or rolls back. Row locking provides the finest grain locking possible and so provides the best possible concurrency and throughput.
If a transaction obtains a lock for a row, then the transaction also acquires a lock for the table containing the row. The table lock prevents conflicting DDL operations that would override data changes in a current transaction. Figure illustrates an update of the third row in a table. Oracle Database automatically places an exclusive lock on the updated row and a subexclusive lock on the table.
TM锁的介绍,
Table Locks (TM)
A table lock, also called a TM lock, is acquired by a transaction when a table is modified by an INSERT, UPDATE, DELETE, MERGE, SELECT with the FOR UPDATEclause, or LOCK TABLE statement. DML operations require table locks to reserve DML access to the table on behalf of a transaction and to prevent DDL operations that would conflict with the transaction.
这是锁类型介绍,不同的锁模式,代表了不同的锁粒度,
问题背景及问题现象
3
先说下我的问题背景,
- 这是高并发的一个 OLTP 应用。
- 应用采用了多线程处理逻辑。
问题现象:
- 数据库层面出现大量 “enq: TX - row lock contention”,行锁等待。
- 应用进程会话,hang 在 DELETE 删除一张父表的操作上,20+ 分钟。
- 在这 20 分钟的期间,非常多的应用进程会话,执行 INSERT 子表记录的时候,提示 ORA-00060 错误,经过统计,总计共有 160 个会话,提示 ORA-00060 错误,换句话说,所有 INSERT 提示 ORA-00060 之后,(1) 中的 DELETE 操作完成。
模拟问题出错过程
4
以下是参考应用日志,创建测试表、外键约束、唯一约束、以及索引,模拟还原了出错的过程,
表:
create table lock_a (id number primary key, name varchar2(1));
create table lock_b (id number primary key, id_a number, id_b number, name varchar2(1));
外键约束:
alter table lock_b add constraint fk_lock foreign key(id_a) references lock_a(id);
唯一约束:
alter table lock_b add constraint unq_lock unique (id_a, id_b);
普通B树的索引:
create index idx_b_01 on lock_b(id_a);
创建测试数据,
SQL> select * from lock_a;
ID NAM
---------- ---
1 a
2 b
SQL> select * from lock_b;
ID ID_A ID_B NAM
---------- ---------- ---------- ---
1 1 1 a
2 1 2 b
3 2 3 x
4 2 4 y
以下操作顺序,是参考实际生产,应用的处理逻辑,以及抛出的错误,
session 1
删除子表外键值为1的记录,
SQL> delete from lock_b where id_a=1;
2 rows deleted.
session ID 是 41,主表子表有锁,
由于未执行提交,会产生一个行锁TX,同时对子表有一个TM锁,由于主外键约束,主表会有一个TM锁,
session 2
INSERT子表记录,注意和现有约束不同,
SQL> insert into lock_b values(10, 1, 10, 'a');
1 row created.
session ID是22,主表子表有锁,
由于未执行提交,会产生一个行锁TX(SID=22),同时对子表有一个TM锁,由于主外键约束,主表会有一个TM锁,
session 3
INSERT子表记录,注意和现有约束不同,
SQL> insert into lock_b(11, 1, 11, 'a');
1 row created
session ID是38,主表子表有锁,
由于未执行提交,会产生一个行锁TX(SID=38),同时对子表有一个TM锁,由于主外键约束,主表会有一个TM锁,
session 1
删除主表ID=1的记录,
SQL> delete from lock_a where id=1;
hang
TX 事务锁时 ID1 和 ID2 的含义如下:
ID1 对应视图 V$TRANSACTION 中的 XIDUSN 字段和 XIDSLOT 字段。其中 ID1 的高16位为 XIDUSN,低16位为 XIDSLOT。
ID2 对应视图 V$TRANSACTION 中的 XIDSQN 字段。
SID=22的TX锁BLOCK=1,SID=41的session有了一条REQUEST=4的TX锁,
上面 ID=3 和 ID=10 两条记录,ID1和ID2相同值,说明正在操作相同的资源,SID=22的 TX 锁(INSERT子表),执行在前,正在阻塞 SID=41 的操作(DELETE主表),执行在后,这个 session 会话处于 hang,等待锁模式为 4 的锁资源。
session 4
此时INSERT子表新记录,注意和之前输入的记录,存在相同的约束,
SQL> insert into lock_b values(11, 1, 11, 'a');
hang
session ID是39,主表子表有锁,
由于未执行提交,会产生一个行锁 TX(SID=39),同时对子表有一个 TM 锁,由于主外键约束,主表会有一个 TM 锁,注意此时,SID=38 的会话 TX 锁 BLOCK=1,当前会话 SID=39,会因此产生一个 REQUEST=4的TX 锁,等待 SID=38 的 TX 锁释放,因为他们的约束值相同,所以当前会话 hang,
session 5
INSERT子表新记录,但是和已有约束不相同,
SQL> insert into lock_b values(12, 1, 12, 'a');
hang
session ID是40,主表子表有锁,
由于未执行提交,会产生一个行锁TX(SID=40),同时对子表有一个TM锁,由于主外键约束,主表会有一个TM锁,注意此时,SID=41的会话TX锁BLOCK=1(DELETE子表),当前会话SID=40,会因此产生一个REQUEST=4的TX锁,等待SID=41的TX锁释放,
session 2
执行提交,
SQL> commit;
Commit complete.
session 1
由于之前 session 1(SID=41) 等待的锁资源,已经释放,所以可以执行最新的语句,删除主表 SQL,但是提示错误,原因就是子表 INSERT 新记录,不能直接删除主表数据,
SQL> delete from lock_a where id=1;
delete from lock_a where id=1
*
ERROR at line 1:
ORA-02292: integrity constraint (BISAL.FK_LOCK) violated - child record found
相应,SID=41之前REQUEST=4的等待就删除了,另外SID=22的3条等待记录,也删除了,
session 3
SQL> commit;
Commit complete.
session 4
SQL> insert into lock_b values(11, 1, 11, 'a');
insert into lock_b values(11, 1, 11, 'a')
*
ERROR at line 1:
ORA-00001: unique constraint (BISAL.SYS_C007067) violated
session 3(SID=38) 提交,3条等待记录会被删除,session 4(SID=39) 的4条等待记录会被删除,
session 1
SQL> delete from lock_b where id_a=1;
1 row deleted.
session 1(SID=41)再次删除子表的记录,
继续删除主表的记录,
SQL> delete from lock_a where id=1;
1 row deleted.
session 5
SQL> insert into lock_b values(12, 1, 12, 'a');
insert into lock_b values(12, 1, 12, 'a')
*
ERROR at line 1:
ORA-00060: deadlock detected while waiting for resource
session 5(SID=40) 抛出 ORA-00060 错误,回滚当前语句,释放锁资源了,因此之前4条等待记录删除,
将以上操作整理为如下表,方便从执行顺序看过程,
OTN上我发了这个问题的帖子,朋友们有兴趣,可以自己看原文(https://community.oracle.com/thread/4110141?start=45&tstart=0),JL大神的回复可谓经典,节录于此,可以有助于更好理解,
回复一:
以上操作,出现问题的是 TX 锁,并不是常见的 TM 锁。如果运行在 RAC 中,即使只是用了单实例,死锁检测的机制,会和单实例不同。
回复二:
At this point session 1 is waiting for session 5 and session 5 is waiting for session 1. Session 5 has been waiting longest so will be the first to detect the deadlock.
上述操作,session 1 等待 session 5 的操作释放资源,session 5 等待 session 1 的操作释放资源,因此出现了死锁,另一个知识点,就是一般情况下(9i以上),等待时间最久的会话,会首先检测死锁,例如上面的session 5会话。
回复三:
之所以主子表,有锁等待,是因为子表需要知道,父表键值是否仍存在,也就是说,子表的执行结果,需要依赖于父表执行,这很容易理解。
关键的一点是:无论手册中出现了几次,TX 锁不是行锁,而是事务锁。优化器不会等待行锁消失,而是需要等待会话的事务锁。可能这是最容易混淆。
我们再回来看下这张表,结合JL所说,对于这次 ORA-00060 错误,推测原因,首先 session 5 等待红色字体的TX资源,这个TX锁资源被 session 1 正占有,(有一个知识点,第4步,session 1 执行删除主表,第7步违反约束,虽然提示错误了,但只会回滚这一句语句,并不会回滚第一步操作,因此 session 5 继续等待 session 1 第一步TX锁资源),此时 session 5 除了等待红色字体 TX,还会占有蓝色字体的 TX 资源,session 1 后面会请求蓝色字体 TX 锁,此时就出现了交叉等待,session 5 等待session 1,session 1 等待 session 5,由于 session 5 是等待最久的会话,所以他会提示 ORA-00060。
除了从上述操作时序外,如果此时检索 ASH,就会看见大量“enq: TX - row lock contention”等待事件,例如 DELETE 操作,从执行处于 hang 状态,直到所有 ORA-00060 抛出,在此期间,等待的就是 enq: TX - row lock contention 事件。
我们再回到问题的现象,以上实验和说明,可以解释的是,为何 INSERT 操作提示 ORA-00060,以及死锁不仅限于两个会话,
- 数据库层面出现大量“enq: TX - row lock contention”,行锁等待。
- 应用进程会话,hang在DELETE删除一张父表的操作上,20+分钟。
- 在这20分钟的期间,非常多的应用进程会话,执行INSERT子表记录的时候,提示ORA-00060错误,经过统计,总计共有160个会话,提示ORA-00060错误,换句话说,所有INSERT提示ORA-00060之后,(1)中的DELETE操作完成。
至于解决方法,我觉得一种是从应用设计上,避免出现 session 1 和 session 5 互等的场景,但这就需要从业务上看,能否满足要求,能否做这种变更。另一种方法,就是是否可以考虑删除外键,因为上述问题,就是因为有外键,所以子表操作依赖父表。但这么做,同样需要考虑其他功能,毕竟外键的存在,就是为了从数据库层面,保证数据的一致,删除外键,就需要从应用层,替代这种功能,能否满足要求,不是一两句话就能决定的。
除此之外,有一个疑问就是,
为何所有 INSERT 提示 ORA-00060,DELETE 才算完成?
其实这就涉及了另一个问题,Oracle 中对于死锁的检测机制。
如果朋友们看过 ORA-00060 产生的 trace,就可以看见会有这么一句,
The following deadlock is not an ORACLE error. It is a deadlock due to user error in the design of an application or from issuing incorrect ad-hoc SQL. The following information may aid in determining the deadlock:
意思就是死锁不是因为 Oracle,一定是由于应用设计,导致使用了不正确的 SQL 语句。
但是 Oracle 妙就妙在,可以自行检测,并释放其中的一个锁资源,让其他事务继续执行。
对于死锁监测的时间,刘大这篇文章,有些对应的实验和解释,非常到位,可以参考《Know more about Enqueue Deadlock Detection》(http://www.askmaclean.com/archives/tag/_enqueue_deadlock_time_sec)。
结论如下,enqueue lock 队列锁的死锁检测遵循以下原则,
- 默认情况下死锁检测deadlock detection总是3s发生一次, 但是实际受到参数_enqueue_deadlock_scan_secs(deadlock scan interval)的控制,该参数默认为0,死锁检测时间总是_enqueue_deadlock_scan_secs指定的值向上取整为3的倍数, 当_enqueue_deadlock_scan_secs=0 则为3s一次, 当_enqueue_deadlock_scan_secs=4则为6s一次,依此类推。
- 死锁检测还受到_enqueue_deadlock_time_sec(requests with timeout <= this will not have deadlock detection)的影响,若enqueue request timeout< _enqueue_deadlock_time_sec(默认值为5),则Server process不会做死锁检测。若enqueue request timeout>_enqueue_deadlock_time_sec 则会参考_enqueue_deadlock_scan_secs定期做死锁检测, 当然request timeout的指定不限于select for update wait [TIMEOUT]一种。
但是要注意上面的结论,基于单实例库,若是RAC,会受隐藏参数_lm_dd_interval(dd time interval in seconds)的影响,11g下默认值为10秒。RAC中,LMD0进程除了管理全局enqueue、资源访问之外,也负责检测全局enqueue死锁。
这个库隐藏参数:
_lm_dd_interval:10
可以简单地这么理解,上述20+分钟DELETE等待的时间,其实就是Oracle解死锁的用时。《DSI-408》中有一句“LMD performs the search, one lock at a time.”,说明了这个检测操作,是串行执行的,160次会话的ORA-00060,160*10=1600s,和20+分钟比较吻合。
当然,死锁的检测不仅仅是这几个隐藏参数能说明的,还是非常复杂的,对我来说还是有些不理解的,但是对于日常问题的判断,最重要的还是,理解死锁产生的原理,不是只有数据的层面,才会发生deadlock,而TX不能从字面含义上,理解为仅是行锁,正确的是事务锁,另外就是,针对主子表(外键),不能惯性地理解TX锁的产生。
下面是《DSI-408》介绍的是“The Classic Deadlock”,进一步明确了,锁资源可以是anything,
The slide shows the classic deadlock scenario. The resources in question could be anything. In the server, they could be rows, tables, ITL slots, or library cache or row cache locks.
This situation can also occur in a RAC cluster, even where the processes are on separate nodes.
原文发布时间为:2018-07-01
本文作者:bisal
本文来自云栖社区合作伙伴“ 数据和云”,了解相关信息可以关注“ 数据和云”。