一、锁的概念
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。加锁是实现数据并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。
- 可防止多个会话同时更改同一数据
- 是在指定语句的最低可能级别自动获取的
- 不会升级
按操作划分,可分为 dml 锁( data locks,数据锁)、 ddl 锁( data dictionary lock )和 system locks。
1、dml 锁保护数据,如表锁锁定整个表,而行锁锁定选定的行。
(1)tm锁(表级锁);
(2)tx锁(事务锁或行级锁)。
2、ddl 锁保护模式对象的结构,如表和视图的字典定义。
3、系统锁保护内部数据库结构,如数据文件。闩锁,互斥锁和内部锁是完全自动的。
latch 是内存中的资源锁
DML锁有如下三种加锁方式:
- 共享锁方式(share):
允许并发查询,但禁止更新锁定的表。需要有 share 锁才能创建表的索引,创建时会自动请求该锁。但是,创建联机索引的操作在建立索引时需要有 row share 锁。
- 独占锁方式(exclusive)
- 共享更新锁(share update)
其中: share、exclusive 用于 tm 锁(表级锁);share update 用于tx锁(行级锁)。
锁模式:
row share(行共享):允许对锁定的表进行并发访问,但禁止在会话中锁定整个表进行独占访问。
SELECT FOR UPDATE 和 SELECT FOR SHARE 命令可在目标表上获得该锁(以及查询中所有引用的表的访问共享锁)。
row exclusive(行独占):与 row share 相同,但是同时禁止以 share 模式锁定。更新、插入或删除数据时会自动获取 row exclusive 锁。row exclusive 锁允许多个进程执行读取, 但只允许一个进程执行写入。
UPDATE、INSERT 和 DELETE 命令在目标表上获得该锁(以及查询中所有引用的表的访问共享锁)。 一般规则是所有修改表的查询获得该锁。
share:
数据并发处理
share row exclusive(共享行独占):用于查询整个表,允许其他人查询表中的行,但禁止其他人在 share 模式下锁定表或更新行。
exclusive(排他):允许查询锁定表,禁止对锁定表执行任何其它活动。需要有 exclusive 锁才能删除表。
latch 属于 system lock, 用于保护 SGA 区中共享数据结构的一种串行化锁定机制,防止多个并发进程同时修改访问某个共享资源。 latch 的实现是与操作系统相关的,尤其和一个进程是否需要等待一个 latch,需要等待多长时间有关。
latch 和 lock
从某种意义上说,latch 是内存中的资源锁,lock是数据库对象(表,索引等)的锁。
导致 latch 争用而等待的原因非常多,内存中很多资源都可能存在争用。 最常见的两类 latch 争用如下:
- 共享池中的 latch 争用。
- 数据缓冲池中的 latch 争用。
最常见的集中共享池里的 latch 是 library cache,可以使用一下语句查询
sql> select * from v$latchname where name like 'library cache%';
资源的争用可以通过如下 sql 来查看:
sql> select event,count(*) from v$session_wait group by event;
数据缓冲池 latch 争用,最常见的 latch 争用有:
- buffer busy waits
- cache buffer chain
cache buffer chian 产生原因:
当一个会话需要去访问一个内存块时,它首先要去一个像链表一样的结构中去搜索这个数据块是否在内存中,当会话访问这个链表的时候需要获得一个 latch,如果获取失败,将会产生 latch cache buffer chain 等待,导致这个等待的原因是访问相同的数据块的会话太多或者这个列表太长(如果读到内存中的数据太多,需要管理数据块的 hash 列表就会很长,这样会话扫描列表的时间就会增加,持有 cache buffer chain latch 的时间就会变长,其他会话获得这个 latch 的机会就会降低,等待就会增加)。
buffer busy waits 产生原因:
当一个会话需要访问一个数据块,而这个数据块正在被另一个用户从磁盘读取到内存中或者这个数据块正在被另一个会话修改时,当前的会话就需要等待,就会产生一个 buffer busy waits 等待。
检查 latch 的相关 sql
查看造成 latch buffer cache chains 等待事件:
select distinct a.owner, a.segment_name
from dba_extents a,
(select dbarfil, dbablk
from x$bh
where hladdr in
(select addr
from (select addr from v$latch_children order by sleeps desc)
where rownum < 20)) b
where a.relative_fno = b.dbarfil
and a.block_id <= b.dbablk
and a.block_id + a.blocks > b.dbablk;
二、导致阻塞的原因
所谓阻塞,就是系统如果平时运行正常,突然会停止不动,多半是被阻塞( blocked ) 住了。
v$lock 这张视图:
select * from v$lock;
我们关注的比较多的是 request 和 block 字段。
如果某个 request 列是一个非 0 值,那么它就是在等待一个锁。
如果 block 列是1,这个 sid 就持有了一个锁,并且阻塞别人获得这个锁。
存在锁请求其实就是被阻塞:
1)DML语句引起的阻塞
insert
update
delete
select .... for update
2)外键没有创建索引
外键没有创建索引
如果系统中有主键/外键引用关系,并且满足以下三个条件中的任意一个,那么就应该考虑给外键字段创建索引,否则系统的性能可能会下降甚至阻塞。
(1) 主表上有频繁的删除操作
(2) 主键上有频繁的修改操作。
(3) 业务上经常会出现主表和从表做关联查询的情况。
三、数据库死锁的检查方法
1) 数据库死锁的现象
程序在执行的过程中,点击确定或保存按钮,程序没有响应,也没有出现报错。
2) 死锁的原理
当对数据库某个表的某一列做更新或删除等操作,执行完毕后该条语句不提交,另一条对于这一列数据做更新操作的语句在执行的时候就会处于等待状态, 此时的现象是这条语句一直在执行,但一直没有执行成功,也没有报错。
3) 死锁的定位方法
通过检查数据库表,能够检查出是哪一条语句被死锁,产生死锁的机器是哪一台。
用dba用户执行以下语句
select
username as 用户,
lockwait as 死锁状态,
status as 状态,
machine as 所在主机,
program as 来自 from v$session
where sid in
(select session_id from v$locked_object);
如果有输出的结果,则说明有死锁,且能看到死锁的机器是哪一台。
字段说明:
username:死锁语句所用的数据库用户;
lockwait:死锁的状态,如果有内容表示被死锁。
status: 状态,active表示被死锁
machine: 死锁语句所在的机器。
program: 产生死锁的语句主要来自哪个应用程序。
用dba用户执行以下语句,可以查看到被死锁的语句
select sql_text from v$sql
where hash_value in
(select sql_hash_value from v$session where sid in
(select session_id from v$locked_object));
四、死锁的解决方法
一般情况下,只要将产生死锁的语句提交就可以了,但是在实际的执行过程中。用户可能不知道产生死锁的语句是哪一句,可以将程序关闭并重新启动就可以了。
经常在oracle的使用过程中碰到这个问题,所以也总结了一点解决方法。
1)查找死锁的进程:
select s.username, l.object_id, l.session_id, s.serial#, l.oracle_username, l.os_user_name, l.process
from v$locked_object l, v$session s
where l.session_id = s.sid;
2)kill掉这个死锁的进程:
alter system kill session 'sid,serial#';
(其中sid=l.session_id)
批量用这条sql(查锁后再杀锁):
select 'alter system kill session
''' || sid || ',' || serial# || ''';'
"deadlock" from v$session where sid in
(select sid from v$lock where block = 1);
注意: 应当注意对于 sid 在 100 以下的应当谨慎,可能该进程对应某个 application,如对应某个事务,可以 kill。
3)如果还不能解决:
select pro.spid from v$session ses, v$process pro where ses.sid=xx and ses.paddr=pro.addr;
例:
select pro.spid from v$session ses, v$process pro where ses.sid=8888 and ses.paddr=pro.addr;
其中sid用死锁的sid替换:
exit
ps -ef|grep spid
sql> !ps -ef|grep 3734
其中spid是这个进程的进程号,kill掉这个oracle进程。
kill -9 spid
五、死锁模拟与死锁问题处理
找锁表的session,并kill 掉之后,对该表的dml 操作正常。
模拟,创建表test
create table test as select * from dba_objects;
开2个session:
session a:
sql>select sid from v$mystat where rownum=1;
session b:
sql> select sid from v$mystat where rownum=1;
session a 更新表test,不commit:
sql> update test set object_id=100 where object_id=20;
2 rows updated.
session b 执行同样的操作,测试session b 会挂住:
sql> update itpux1 set object_id=100 where object_id=20;
在session a commit 之前,一直处于等待状态..
select * from dba_blockers;
select sid,serial# from v$session where sid=1;
alter system kill session '1,7' immediate;
可以查到session a有死锁情况
查看表上锁的情况:
select sn.username, m.sid, sn.serial#, m.type, decode
(m.lmode,0,'none',1,'null',2,'rowshare',3,'rowexcl.',4,'share',5,'s/rowexcl.',6,'exclusive', lmode, ltrim (to_char (lmode, '990')))
lmode, decode (m.request,0,'none',1,'null',2, 'rowshare', 3, 'rowexcl.',
4,'share',5,'s/rowexcl.',6,'exclusive', request, ltrim (to_char (m.request, '990')))
request, m.id1,m.id2
from v$session sn, v$lock m
where
(sn.sid = m.sid and m.request != 0) or (sn.sid = m.sid and m.request = 0
and
lmode != 4 and (id1, id2) in (select s.id1, s.id2 from v$lock s where request != 0
and
s.id1 = m.id1 and s.id2 = m.id2))
order by
id1, id2, m.request;
这里就显示了锁的信息, tx 是行级 exclusive 锁。
查看v$lock, 可以验证以上锁的信息:
select * from v$lock where sid in (11,26);
request 是申请锁资源
block:如果是1,就代表该该sid 就持有了一个锁,并且阻塞别人获得这个锁。
在session a 提交:
sql> commit;
commit complete.
这时候session b 等待的事务完成了完成:
sql> update itpux1 set object_id=100 whereobject_id=20;
0 rows updated.
阻塞已经结束。 如果找不到对应的session 来进行commit 操作,那就只能kill session了。
因为我这是测试库,所以也是用kill session来进行的。
sql>alter system kill session 'sid,serial#';