常见的死锁是如何产生的,如何避免

概述:

什么场景下回发生 00060 死锁问题:

​一般情况下,数据库自身发生死锁的情况很少,一般情况都是因为应用本身调用问题导致的 00060异常

​比如说有两个会话sid,分别为 138 和136,这两个会话都要对 6677 和 7788 两个人加工资,但是执行的顺序不一样,操作分别是:

-- 会话session号 Session 1 (sid = 136), Session 2 (sid = 138)

-- 执行的语句 Session 1 (sid = 136)
update emp set sal=sal+100 where empno=6677;
update emp set sal=sal+100 where empno=7788;

-- 执行的语句 Session 2 (sid = 138)
update emp set sal=sal+100 where empno=7788;
update emp set sal=sal+100 where empno=6677;

ORA-00060:deadlock detected while waiting for resource
这样我们就成功的触发了一个ORA-00060。

出现这个问题,我们可以查看Oracle日志,日志路径:$ORACLE_BASE/diag/rdbms/org11/ora11/trace/alert*.log

可以从告警日志中看到很多类似如下的日志:

ORA-00060:Deadlock detected.More info in file /home1/oracle/diag/rdbms/ora11g/ora11g/trace/ora11g_ora_14757.trc

我们看下对饮的trc日志,主要看Deadlock graph,其中:可以看到136和138互相死锁,session 138(也就是 session2,sid=138)等着要 ROWID=AAAMfAAAAgAAA的行锁,而 session136(也就是 session1,sid=136)等着要ROWID=AAAMfPAAEAAAAgAAL 的行。

处理方式:

1. 给资源编号,然后按照固定的顺序进行访问。

​简单的来说,就是先改编号小的,在改编号大的。当然,反着来也可以。

-- 会话session号 Session 1 (sid = 136), Session 2 (sid = 138)

update emp set sal=sal+100 where empno=6677;
update emp set sal=sal+100 where empno=7788;

Waiting...

update emp set sal=sal+100 where empno=7788;

commit/rollback;

update emp set sal=sal+100 where empno=6677;

​在这里,工资增加两次,但是 session 2 被 session 1 阻塞了,对于用户体验来说,感受不好。

​如果 session 1 一直不结束事务,session 2 只能一直等下去,这样比deadlock 后,Oracle 程序本身出面调停还要糟糕。

2. 可以在 select … for update nowait 语句测试一下需要更改的行是否被锁定

​如果没有被锁定,那这个语句会马上给这行加锁,如果已经加锁那就马上返回:ORA-00054:resource busy and acquire with NOWAIT specified ,如下表所示:

-- 会话session号 Session 1 (sid = 136), Session 2 (sid = 138)

select * from emp where empno in(6677,7788) for update nowait;

select * from emp where empno in(6677,7788) for update nowait;

ORA-00054:resource busy and acquire with NOWAIT specified

update emp set sal=sal+100 where empno=6677;
update emp set sal=sal+100 where empno=7788;

方法一和方法二都存在一定的问题,特别是在ND代码中如果使用方法二那么修改起来工作量太大,但是如果我们不处理,Oracle有自动检测死锁并且回滚事务的功能,也就是说之前的会话中136 和138 有一个会成功,一个会回滚,返回失败,这样就保证了数据的一致性。

总结:

对应上面两处处理方式。感觉都不好,毕竟现网这种场景较少。而且这种死锁不是永久性的一直卡死在这,Oracle会检测到这种死锁的,并且检测到后会自己回滚,所以直接交给Oracle即可。

附相关的查询SQL:

  1. 查询死锁:

    select t2.username,t2.sid,t2.seria#,t2.logon_time 
    from v$locked_object t1,v$session t2
    where t1.session_id = t2.sid
    order by t2.logon_time;
    
  2. 根据 sid 查询对应的SQL语句,比如第一点查询出 sid 为136 和138 的死锁结果:

    select sql_text
    from v$session a,$sqltext_with_newlines b
    where DECODE(a.sql_hash_value,0,prev_hash_value,sql_hash_value) = b.hash_value and a.sid in ('136','138')
    order by piece;
    
  3. 查看处于等待状态的SQL语句:

    select a.spid,c.EVENT,b.LOGON_TIME,d.SQL_TEXT,a.PROGRAM
    from v$process a,v$session b,v$session_wait c,v$sql d
    where a.ADDR = b.PADDR and b.SID = c.SID
      and b.SQL_HASH_VALUE = D.HASH_VALUE
      and c.EVENT NOT LIKE '%SQL*Net%'
      and c.EVENT NOT LIKE '%smon%'
      and c.EVENT NOT LIKE '%jopq%'
      and c.EVENT NOT LIKE '%ipc%'
    
  4. 查看目前是否有“长时间持有锁未释放”,必要的情况下可以用对应的 command 杀死 session:

    select I.BLOCK,ILMODE,I.REQUEST,I.TYPE,I.ID1,I.CTIME,s.SID,s.SERIAL#,
    T.SQL_TEXT,p.SPID,'ALTER SYSTEM KILL SESSION'"||S.SID||','||s.serial#||"',' Command
    from v$lock i,v$SESSION s,v$SQL t,v$process p
    where I.ID1 in (select id1 from v$lock where block = 1)
      and I.SID = s.SID and (t.hash_value = DECODE(s.sql_hash_value,0,s.prev_hash_value,s.sql_hash_value))
      and t.address = DECODE(a.sql_hash_value,0,s.prev_hash_addr,s.sql_address))
      and p.ADDR = s.PADDR
    order by I.ID1,I.CTIME desc;
    
  5. 杀死进程(396为sid,60589为serial#),该条可以结合第一点进行:

    alter system kill session '396,60589';
    
02-12 08:21