Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

0    114    1

Tags:

👉 本文共约9683个字,系统预计阅读时间或需37分钟。

前言部分

导读和注意事项

各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不知道的知识,~O(∩_∩)O~:

① enq: TX - allocate ITL entry等待事件的解决

② 一般等待事件的解决办法

③ 队列等待的基本知识

④ ITL死锁解决

⑤ ITL死锁模拟

⑥ Merge语句的非关联形式的查询优化

故障分析及解决过程

故障环境介绍

项目source db
db 类型RAC
db version11.2.0.3.0
db 存储ASM
OS版本及kernel版本AIX 64位 7.1.0.0

故障发生现象及故障分析解决

早上刚来上班,同事就发了一个SQL过来,说是有锁,然后我就查了查系统里的锁,结果一个锁都没得。好吧,还是得干点事的,先看看SQL语句:

MERGE INTO TLHR.TLHRBOKBAL S

USING (SELECT A.BOOKACCOUNT AS BOOKACCOUNT,

(A.CURRBALANCE + NVL(B.BAL, 0.00)) AS BANKAMT

FROM TLHR.TLHRBOKBAL_TMP A,

(SELECT T1.BOOKACCOUNT AS BOOKACCOUNT,

SUM(DECODE(T1.DCFLAG, 'D', -T1.AMT, 'C', T1.AMT, 0)) AS BAL

FROM TLHR.TLHRBOKBALJN T1

WHERE T1.BOOKACCOUNT LIKE '13500000%'

AND T1.TRANDATE = '20150901'

AND (T1.REASON IN ('1', '2') OR

(T1.REASON = '0' AND T1.ONLINEFLAG = '1'))

GROUP BY T1.BOOKACCOUNT) B

WHERE A.BOOKACCOUNT = B.BOOKACCOUNT(+)

AND A.BOOKACCOUNT LIKE '13500000%') T

ON (S.BOOKACCOUNT = T.BOOKACCOUNT)

WHEN MATCHED THEN

UPDATE

SET S.LASTBALANCE = T.BANKAMT,

S.CURRBALANCE = T.BANKAMT,

S.DEBITAMT = 0.00,

S.CREDITAMT = 0.00;

看起来是一个MERGE语句,按照小麦苗以前的经验,这一类的SQL最好是修改为MERGE的非关联形式比较好,我们先看看执行计划有没有问题:

先找到SQL_ID为53qv858pwwwwb:

SELECT a.ELAPSED_TIME,a.EXECUTIONS,a.* FROM v$sql a WHERE a.SQL_TEXT LIKE '%MERGE INTO TLHRBOKBAL S%' AND A.SQL_TEXT LIKE '%13500000%' ;

查询历史执行计划:

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR(SQL_ID => '53qv858pwwwwb' )) ;

可以看到,该执行计划的顺序为【7-->6-->5-->4-->9-->10-->8-->3-->2-->1-->0】,而耗费性能的地方在9、10、8这3个步骤上,走的是全表扫描,我们先看看2个大表的数据量:

SELECT COUNT(*) FROM TLHR.TLHRBOKBAL_TMP A WHERE A.BOOKACCOUNT LIKE '13500000%'; --306043/38998765

SELECT COUNT(*) FROM TLHR.TLHRBOKBAL A WHERE A.BOOKACCOUNT LIKE '13500000%'; --306043/38826275

从3000万的数据里边取出30万的数据,还是比较少的,所以应该去走索引的,看了一下统计信息,也是最新收集的,好吧,算了,先修改一下SQL让其走索引扫描看看,:

MERGE INTO TLHR.TLHRBOKBAL S

USING (SELECT S.ROWID ROWIDS,

A.BOOKACCOUNT AS BOOKACCOUNT,

(A.CURRBALANCE + NVL(B.BAL, 0.00)) AS BANKAMT

FROM (SELECT /*+index(NB,PK_TLHRBOKBAL_TMP)*/NB.CURRBALANCE,NB.BOOKACCOUNT

FROM TLHR.TLHRBOKBAL_TMP NB

WHERE NB.BOOKACCOUNT LIKE '13500000%') A,

TLHR.TLHRBOKBAL S,

(SELECT T1.BOOKACCOUNT AS BOOKACCOUNT,

SUM(DECODE(T1.DCFLAG, 'D', -T1.AMT, 'C', T1.AMT, 0)) AS BAL

FROM TLHR.TLHRBOKBALJN T1

WHERE T1.BOOKACCOUNT LIKE '13500000%'

AND T1.TRANDATE = '20150901'

AND (T1.REASON IN ('1', '2') OR

(T1.REASON = '0' AND T1.ONLINEFLAG = '1'))

GROUP BY T1.BOOKACCOUNT) B

WHERE A.BOOKACCOUNT = B.BOOKACCOUNT(+)

AND S.BOOKACCOUNT = A.BOOKACCOUNT

AND S.BOOKACCOUNT LIKE '13500000%') T

ON (T.ROWIDS = S.ROWID)

WHEN MATCHED THEN

UPDATE

SET S.LASTBALANCE = T.BANKAMT,

本人提供Oracle、MySQL、PG等数据库的培训和考证业务,私聊QQ646634621或微信db_bao,谢谢!

S.CURRBALANCE = T.BANKAMT,

S.DEBITAMT = 0.00,

S.CREDITAMT = 0.00

执行计划中,基本都走了索引了,跑了一下,大约1分种多,但是里边有个HINTS,分析了一下表TLHRBOKBAL_TMP上的索引情况,发现是个主键索引,且有2个列(BOOKACCOUNT,CURRENCY),但是不包含列CURRBALANCE,可能是Oracle觉得回表读的耗费比较大吧,那这里可以使用虚拟索引测试一下索引的性能:

CREATE INDEX IX_VI01_ID ON TLHR.TLHRBOKBAL_TMP(CURRBALANCE, CURRENCY,BOOKACCOUNT) NOSEGMENT;

ALTER SESSION SET "_USE_NOSEGMENT_INDEXES"=TRUE;

EXPLAIN PLAN FOR MERGE INTO TLHR.TLHRBOKBAL S

USING (SELECT S.ROWID ROWIDS,

A.BOOKACCOUNT AS BOOKACCOUNT,

(A.CURRBALANCE + NVL(B.BAL, 0.00)) AS BANKAMT

FROM (SELECT NB.CURRBALANCE,NB.BOOKACCOUNT

FROM TLHR.TLHRBOKBAL_TMP NB

WHERE NB.BOOKACCOUNT LIKE '13500000%') A,

TLHR.TLHRBOKBAL S,

(SELECT T1.BOOKACCOUNT AS BOOKACCOUNT,

SUM(DECODE(T1.DCFLAG, 'D', -T1.AMT, 'C', T1.AMT, 0)) AS BAL

FROM TLHR.TLHRBOKBALJN T1

WHERE T1.BOOKACCOUNT LIKE '13500000%'

AND T1.TRANDATE = '20150901'

AND (T1.REASON IN ('1', '2') OR

(T1.REASON = '0' AND T1.ONLINEFLAG = '1'))

GROUP BY T1.BOOKACCOUNT) B

WHERE A.BOOKACCOUNT = B.BOOKACCOUNT(+)

AND S.BOOKACCOUNT = A.BOOKACCOUNT

AND S.BOOKACCOUNT LIKE '13500000%') T

ON (T.ROWIDS = S.ROWID)

WHEN MATCHED THEN

UPDATE

SET S.LASTBALANCE = T.BANKAMT,

S.CURRBALANCE = T.BANKAMT,

S.DEBITAMT = 0.00,

S.CREDITAMT = 0.00;

SELECT * FROM TABLE(DBMS_XPLAN.display);

说明创建3个列的索引是可以的。我们先将该虚拟索引删除DROP INDEX IX_VI01_ID;

ITL死锁问题解决

另外一个问题,是开发说上边的SQL语句产生了死锁,起初我还半信半疑,先去告警日志中用命令(more alert* | grep Deadlock)搜了一下:

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

结果发现很多的死锁,拿到相关的文件,看到如下一段:

果然,产生死锁的SQL还是上边分析优化的SQL,其中会话信息为:(332,47221),我们去DBA_HIST_ACTIVE_SESS_HISTORY视图里查询:

SELECT D.SQL_ID, D.CURRENT_OBJ#, D.EVENT, COUNT(1)

FROM DBA_HIST_ACTIVE_SESS_HISTORY D

WHERE D.SAMPLE_TIME BETWEEN

TO_DATE('2016-09-01 18:25:00', 'YYYY-MM-DD HH24:MI:SS') AND

TO_DATE('2016-09-01 18:45:00', 'YYYY-MM-DD HH24:MI:SS')

AND D.BLOCKING_SESSION_STATUS = 'VALID'

AND D.SESSION_ID = 332

AND D.SESSION_SERIAL# = 47221

GROUP BY D.SQL_ID, D.CURRENT_OBJ#, D.EVENT;

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

可以看到该会话的等待事件是enq: TX - allocate ITL entry。可以猜测是由于ITL事务槽引起的问题。

SELECT DISTINCT D.BLOCKING_SESSION, D.BLOCKING_SESSION_SERIAL#, D.SQL_ID

FROM DBA_HIST_ACTIVE_SESS_HISTORY D

WHERE D.SAMPLE_TIME BETWEEN

TO_DATE('2016-09-01 18:25:00', 'YYYY-MM-DD HH24:MI:SS') AND

TO_DATE('2016-09-01 18:45:00', 'YYYY-MM-DD HH24:MI:SS')

AND D.EVENT = 'enq: TX - allocate ITL entry'

AND D.BLOCKING_SESSION_STATUS = 'VALID'

AND D.SESSION_ID = 332

AND D.SESSION_SERIAL# = 47221;

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

可以看出会话(332,47221)共阻塞了3个会话,由于有死锁,那么我们看看上边查询出来的3个会话阻塞了哪些会话:

SELECT DISTINCT D.INSTANCE_NUMBER,

D.SESSION_ID,

D.SESSION_SERIAL#,

D.BLOCKING_INST_ID,

D.BLOCKING_SESSION,

D.BLOCKING_SESSION_SERIAL#,

D.SQL_ID

FROM DBA_HIST_ACTIVE_SESS_HISTORY D

WHERE D.SAMPLE_TIME BETWEEN

TO_DATE('2016-09-01 18:25:00', 'YYYY-MM-DD HH24:MI:SS') AND

TO_DATE('2016-09-01 18:45:00', 'YYYY-MM-DD HH24:MI:SS')

AND D.EVENT = 'enq: TX - allocate ITL entry'

AND D.BLOCKING_SESSION_STATUS = 'VALID'

AND ((D.SESSION_ID = 332 AND D.SESSION_SERIAL# = 47221) OR

(D.SESSION_ID = 2602 AND D.SESSION_SERIAL# = 4343) OR

(D.SESSION_ID = 2995 AND D.SESSION_SERIAL# = 46891) OR

(D.SESSION_ID = 1894 AND D.SESSION_SERIAL# = 30761));

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

可以看到,1894和2602相互阻塞(绿色表示),332和2602相互阻塞(红色表示),2995和332相互阻塞(粉色表示),这么多的相互阻塞就产生了死锁,这里由于SQL_ID不同,而且产生的等待事件是enq: TX - allocate ITL entry,所以推测出生成的是ITL死锁。

解决这类问题就是增大ini_trans和PCT_FREE的值。

SELECT * FROM DBA_TABLES D WHERE D.TABLE_NAME = 'TLHRBOKBAL';

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

可以看到,ini_trans和PCT_FREE的值都是默认的,太小了,根据MOS(Troubleshooting waits for 'enq: TX - allocate ITL entry' (Doc ID 1472175.1) 地址:http://blog.itpub.net/26736162/viewspace-2124531/)我们需要修改该参数,SQL如下:

ALTER TABLE TLHR.TLHRBOKBAL PCTFREE 20 INITRANS 16 ;

ALTER TABLE TLHR.TLHRBOKBAL MOVE NOLOGGING PARALLEL 12;

ALTER TABLE TLHR.TLHRBOKBAL LOGGING NOPARALLEL;

ALTER INDEX TLHR.PK_TLHRBOKBAL REBUILD PCTFREE 20 INITRANS 16 NOLOGGING PARALLEL 12;

ALTER INDEX TLHR.PK_TLHRBOKBAL LOGGING NOPARALLEL;

由于表里有3000W的数据量,开了并行,本来我预估的是5分钟,结果move表的时候10秒都不到还是比较快的。

调整之后的值:

SELECT * FROM DBA_TABLES D WHERE D.TABLE_NAME = 'TLHRBOKBAL';

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

SELECT * FROM dba_indexes d WHERE d.index_name='PK_TLHRBOKBAL';

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

修改已经生效,接下来就看开发那边是否还报死锁的错误,这个等待需要明天看了。

终于等到第2天了,看来没有报错了:

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

这里我们模拟一个ITL死锁

有人的地方就有江湖,有资源阻塞的地方就可能有死锁。所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。其最常见的死锁的类型分为:行级锁(row-level locks)和块级锁(block-level locks),这里的行级锁其实就是指的ITL死锁。有关死锁的问题,有许多需要介绍的,这篇blog主要是故障处理,所以这里我们模拟一个ITL死锁产生的过程即可,后边我会系统的发一次有关死锁的内容,还有ITL的内容,希望大家持续关注小麦苗的微信公众号(DB宝)。

实验部分:

实验的设计过程来源于网络!

我们首先创建一张表T_ITL_LHR,这里指定PCTFREE为0,INITRANS为1,就是为了观察到ITL的真实等待情况,然后我们给这些块内插入数据,把块填满,让它不能有空间分配。

我们检查数据填充的情况:

可以发现,这2000条数据分布在3个块内部,其中有2个块(94953和94954)填满了,一个块(94955)是半满的。因为有2个ITL槽位,我们需要拿2个满的数据块,4个进程来模拟ITL死锁:

实验步骤会话SID要更新的块号要更新的行号是否有阻塞
步骤一119949531N
步骤一279949532N
步骤一378949541N
步骤一4139949542N

这个时候系统不存在阻塞,

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

以上4个进程把2个不同块的4个ITL槽位给消耗光了,现在的情况,就是让他们互相锁住,达成死锁条件,回到会话1,更新块94954,注意,以上4个操作,包括以下的操作,更新的根本不是同一行数据,主要是为了防止出现的是行锁等待。

实验步骤会话SID要更新的块号要更新的行号是否有阻塞
步骤一119949531N
步骤一279949532N
步骤一378949541N
步骤一4139949542N
步骤二119949543Y
步骤二378949533Y

会话1:

UPDATE T_ITL_LHR SET A=A

WHERE DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)=94954

AND DBMS_ROWID.ROWID_ROW_NUMBER(ROWID)=3;

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

会话1出现了等待。

会话3:

UPDATE T_ITL_LHR SET A=A

WHERE DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)=94953

AND DBMS_ROWID.ROWID_ROW_NUMBER(ROWID)=3;

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

会话3发现出现了等待。

我们查询阻塞的具体情况:

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

可以看到,会话1被会话4阻塞了,会话3被会话2阻塞了。

注意,如果是9i,在这里就报死锁了,但是在10g里面,这个时候,死锁是不会发生的,因为这里的会话1还可以等待会话4释放资源,会话3还可以等待会话2释放资源,只要会话2与会话4释放了资源,整个环境又活了,那么我们需要把这两个进程也塞住。

出现的是行锁等待。

实验步骤会话SID要更新的块号要更新的行号是否有阻塞
步骤一119949531N
步骤一279949532N
步骤一378949541N
步骤一4139949542N
步骤二119949543Y
步骤二378949533Y
步骤三279949544Y
步骤三4139949534Y

会话2,注意,我们也不是更新的同一行数据:

UPDATE T_ITL_LHR SET A=A

WHERE DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)=94954

AND DBMS_ROWID.ROWID_ROW_NUMBER(ROWID)=4;

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

会话2出现了等待,具体阻塞情况:

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

我做了几次实验,会话2执行完SQL后,会话3到这里就报出了死锁,但有的时候并没有产生死锁,应该跟系统的阻塞顺序有关,若没有产生死锁,我们可以继续会话4的操作。

会话4,注意,我们也不是更新的同一行数据:

UPDATE T_ITL_LHR SET A=A

WHERE DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)=94953

AND DBMS_ROWID.ROWID_ROW_NUMBER(ROWID)=4;

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

会话4发现出现了等待。

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

虽然,以上的每个更新语句,更新的都不是同一个数据行,但是,的确,所有的进程都被阻塞住了,那么,死锁的条件也达到了,等待一会(这个时间有个隐含参数来控制的:_lm_dd_interval),我们可以看到,会话2出现提示,死锁:

报出死锁之后的阻塞情况:

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

我们可以在会话2上继续执行步骤三中的SQL,依然会产生死锁。生成死锁后,在告警日志中有下边的语句:

其中的内容有非常经典的一段Global Wait-For-Graph(WFG):

该实验过程可能有点复杂,小麦苗画了个图来说明整个实验过程:

Oracle等待事件队列等待之TX - allocate ITL entry引起的死锁处理

标签:

头像

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复

嘿,我是小麦,需要帮助随时找我哦
  • 18509239930
  • 个人微信

  • 麦老师QQ聊天
  • 个人邮箱
  • 点击加入QQ群
  • 个人微店

  • 回到顶部
返回顶部