锁的分类
没有并发就没有锁
锁分类:
- Enqueues:队列类型的锁,通常和业务的DML操作相关。平时说Lock锁时通常指的是Enqueues。
- Latches:系统资源方面的锁,比如内存结构、SQL解析…
锁的原则
只有被修改时,行才会被锁定。
当一条语句修改了一条记录,只有这条记录上被锁定,在Oracle数据库中不存在锁升级。
有些品牌的数据库,如果一个表中有很多行都加了行锁,就会升级为对这个表的表锁。Oracle没有锁升级的策略。
当某行被修改时,它将阻塞别人对它的修改。
当一个事务修改一行时,将在这个行上加上行锁(TX),用于阻止其他事务对相同行的修改。
读永远不会阻止写。
读不会阻塞写,但是有唯一的一个例外,就是:select ... for update
写永远不会阻塞读。
当一行被修改后,Oracle通过回滚段提供给数据的一致性读。
锁的类型
通过视图v$lock_type
查询所有锁的类型。
select * from v$lock_type;
主要用到的是TM锁和TX锁:
- TM表锁:发生在 insert、update、delete以及 select for update操作时,目的是保证操作能够正常进行,并且阻止其他人对表执行DDL操作。
- TX事务锁(行锁):对于正在修改的数据,阻止其他会话进行修改。
在执行一条update语句时,只有被修改的那一行上面会加一个TX锁,而在被操作的整张表上会加一个TM锁。
锁的阻塞
环境搭建:
create table t(id int primary key);
insert into t values(1);
insert into t values(2);
update操作
通过v$lock
查看数据库的锁:
第一个用户:
select distinct sid from v$mystat; -- 查看当前会话的SID,例如 160
update t set id=3 where id=2; -- 执行一条update语句,但是不提交
第二个用户:
select distinct sid from v$mystat; -- 查看当前会话SID,例如66
update t set id=1 where id=2; -- 因为160会话没有进行commit,所以该会话执行update时卡住不动
通过v$lock
表查看数据库的锁:
select sid,type,id1,id2,lmode,request,ctime,block from v$lock where type in ('TM', 'TX'); -- 我们只关注TM表锁和TX行锁
查询出来的内容:
SID | Type | ID1 | ID2 | LMode | Request | CTime | Block |
---|---|---|---|---|---|---|---|
66 | TX | 131104 | 15670 | 0 | 6 | 200 | 0 |
66 | TM | 93015 | 0 | 3 | 0 | 200 | 0 |
160 | TX | 131104 | 15670 | 6 | 0 | 209 | 0 |
160 | TM | 93015 | 0 | 3 | 0 | 209 | 1 |
每个会话执行update
时都产生了一个TM、一个TX锁。
TM锁的类型是3(共享锁),所以66和160会话可以同时都持有。
160会话的TX锁的LMode是6,表示160会话持有这一行的排他锁。
66会话的Lmode是0,Request正在请求得到Mode为6的行锁,表示其没有持有这个行锁。
160会话的Block为1,表示其阻塞了一个会话。
TM锁的 ID1 表示这个TM锁对应的表在dba_objects
中的编号:
select * from dba_objects where object_id='93015';
TX锁的 ID1 和 ID2 经过位运算,得到的是该TX锁定的回滚段上的位置。
通过v$session_wait
查看会话的锁状态:
select sid, event from v$session_wait where sid in (66,160);
SID | Event | 备注 |
---|---|---|
66 | enq: TX - row lock contention | 等待获取TX行锁 enq表示Enqueues锁 |
160 | SQL*Net message from client |
delete操作
delete操作的锁持有情况和update相同。
insert操作
将之前会话的操作都进行commit。
160会话执行一条insert操作:
insert into t values(1); -- insert之后没有进行commit提交
66会话执行insert操作:
insert into t values(2);
commit; -- 插入一条和160会话不冲突的数据,可以正常的插入并提交
insert into t values(1); -- 插入一条河160会话相同主键的数据,因为160会话没有提交,所以此处会卡住
此时,查看v$lock
中对应的锁:
select * from v$lock where type in ('TM','TX');
SID | Type | ID1 | ID2 | LMode | Request | Block |
---|---|---|---|---|---|---|
66 | TX | 589851 | 15892 | 6 | 0 | 0 |
66 | TX | 655360 | 30385 | 0 | 4 | 0 |
66 | TM | 93015 | 0 | 3 | 0 | 0 |
160 | TX | 655360 | 30385 | 6 | 0 | 0 |
160 | TM | 93015 | 0 | 3 | 0 | 1 |
此时可以发现,66号会话除了一条正在申请4类型TX锁之外, 还多出了一条持有6类型锁的记录。
对于update、delete操作的都是同一条数据。但是对于insert,这两个会话插入的实际上是两条数据,只是这两条数据的值相同,而主键的约束又不允许存在两条相同主键的数据,所以产生了这个阻塞,而不是数据修改的阻塞。所以此处66号会话申请的是4模式的锁(比6模式锁的级别低)。
select for update操作
将之前会话的操作进行commit。
160会话执行select for update操作:
select * from t where id=2 for update; -- 此时不进行commit
66会话执行update操作:
update t set id=4 where id=2; -- 因为160会话没有提交,所以此处会卡住
此时查看v$lock
中锁状态,同update操作的锁相同。
锁的模式Lock Mode
Lock Mode:
- 2:Row Share(RS)
- 3:Row Exclusive Table Lock(RX)
- 4:Share Table Lock(S)
- 5:Share Row Exclusive Table Lock (SRX)
- 6:Exclusive Table Lock(X)
6的限制性最强
模式互斥关系:
模式 | 加锁的SQL语句 | 排斥的模式 | 允许的DML |
---|---|---|---|
2 | lock table t in row share mode | 6 | select、insert、update、delete、for update |
3 | lock table t in row exclusive mode | 4,5,6 | select、insert、update、delete、for update |
4 | lock table t in share mode | 3,5,6 | select |
5 | lock table t in share row exclusive mode | 3,4,5,6 | select |
6 | lock table t in exclusive mode | 2,3,4,5,6 | select |
示例:
160会话手工为t表添加一个4模式的锁:
lock table in share mode; -- 没有commit,此时160会话会为t表添加一个4类型锁,其他会话只允许select操作
66会话此时执行insert:
insert into t values(6); -- 因为160会话添加了锁,所以66会话的insert会卡住
此时查看v$lock
表:
select * from v$lock where type in ('TM', 'TX');
SID | Type | ID1 | ID2 | LMode | Request | Block |
---|---|---|---|---|---|---|
66 | TM | 93015 | 0 | 0 | 3 | 0 |
160 | TM | 93015 | 0 | 4 | 0 | 1 |
此时160会话持有4模式的锁。66会话在请求3模式的锁。160的Block显示其阻塞了一个会话。
160会话执行commit操作后即会释放锁。
示例2:
160会话手工添加4模式的锁,然后insert一条数据:
lock table in share mode;
insert into t values(5); -- 此时没有commit;
66会话insert一条河160会话不同的数据:
insert into t values(6); -- insert的数据和160会话不同,但是因为160会话手工加了4模式锁,所以此处也会卡住
此时查看v$lock
表:
select * from v$lock where type in ('TM', 'TX');
SID | Type | ID1 | ID2 | LMode | Request | Block |
---|---|---|---|---|---|---|
66 | TM | 93015 | 0 | 0 | 3 | 0 |
160 | TM | 93015 | 0 | 5 | 0 | 1 |
160 | TX | 262153 | 15443 | 6 | 0 | 0 |
160会话因为执行了insert,所以多出来了一条TX锁,但是该锁的Block为0,并没有阻塞到66会话。
160会话执行insert后,手工添加的4模式锁变成了5模式锁。因为执行insert时,oracle数据库需要在该表上添加一个3模式锁,但是已经存在了4模式锁,4模式锁和3模式锁是互斥关系无法添加,所以Oracle将4模式锁升级成了更严格的5模式锁。
手工锁定表的应用:
lock table dept in share mode; -- 后面几条sql都需要用到dept表,防止几条sql执行过程中dept表被其他用户修改,此处手工将dept表锁定,防止被其他用户修改
update emp set sal=sal*1.1 where deptno in (select deptno from dept where loc='DALLAS');
update budget set totsal=totsal*1.1 where deptno in (select deptno from dept where loc='DALLAS');
commit; -- 释放锁
RI锁
基于外键引用关系的锁定
当对具有主外键关系的表做DML操作时,锁定不单单发生在操作表上,相应的引用表上也可能加上相应的锁定。
环境搭建:
create table p(id int primary key); -- 主表
create table c(id references p(id)); -- 从表,外键指向p表的id
向p表插入一条数据:
insert into p values(1); -- 向主表插入一条数据,不进行commit;
查看v$lock
视图:
select * from v$lock;
SID | Type | ID1 | ID2 | LMode | Request | Block |
---|---|---|---|---|---|---|
160 | TM | 93017 | 0 | 3 | 0 | 0 |
160 | TM | 93019 | 0 | 3 | 0 | 0 |
160 | TX | 458752 | 15611 | 6 | 0 | 0 |
93017对象是P表,93019对象是C表。
向主表插入一条数据后,oracle不仅在主表加了锁,还在从表上也加上了锁。
commit之后,更新p表的数据:
update t set id=2 where id=1; -- 更新主表的一条数据,不进行commit;
查看v$lock
视图:
select * from v$lock;
SID | Type | ID1 | ID2 | LMode | Request | Block |
---|---|---|---|---|---|---|
160 | TM | 93017 | 0 | 3 | 0 | 0 |
160 | TX | 589855 | 15893 | 6 | 0 | 0 |
update时,oracle只在主表上加了锁,没有在从表上加锁。
主表的delete操作也不会在从表上加锁,只在主表上加锁。
从表的insert、delete、update操作,除了在从表上加锁,也都会在主表上加锁。
160会话向主表插入一条数据:
insert into p values(1); -- 不进行commit;
66会话向从表插入一条数据:
insert into c values(1); -- c中插入的数据需要引入p表的id,因为160会话还没有提交,所以p表是否有id=1这条数据还不确定。此处会卡住
查看v$lock
:
select * from v$lock where type in ('TM', 'TX');
SID | Type | ID1 | ID2 | LMode | Request | Block |
---|---|---|---|---|---|---|
66 | TM | 93017 | 0 | 3 | 0 | 0 |
66 | TM | 93019 | 0 | 3 | 0 | 0 |
66 | TX | 131091 | 15667 | 0 | 4 | 0 |
66 | TX | 458757 | 15612 | 6 | 0 | 0 |
160 | TM | 93019 | 0 | 3 | 0 | 0 |
160 | TM | 93017 | 0 | 3 | 0 | 0 |
160 | TX | 131091 | 15667 | 6 | 0 | 1 |
类似于两个会话同时向同一张表插入相同主键的数据,后插入的那个会话(66会话)会多出一个TX申请4模式的锁。
160会话向主表插入数据时,会在主表、从表、插入数据回滚段三个地方加锁。
66会话向从表插入数据时,会在主表、从表、插入数据的回滚段三个地方加锁。
因为160会话没有提交,所以66会话还需要申请引用主表的外键的数据回滚段的4模式锁。
在Oracle 9i、10g时,向从表插入一条数据,也会在主表上加锁,但是向主表加的是2模式的锁。在11g时,提高到了3模式的锁。
同样的,向主表插入数据时,9i、10g在从表上加的锁是2模式的。在11g时,提高到了3模式。
表的外键最好带有索引。如果外键上没有索引,当oracle在向主表插入或更新数据时,要同时锁定从表,就要进行全表扫描,在全表扫描期间,这个会话是一直持有锁的,容易引起阻塞。
如果外键带有索引,在修改主表的主键时,可以从索引中很快查出从表中是否引用了这个主键,如果有引用直接报错不允许修改。
死锁
两个会话互相持有对象资源导致死锁。
示例:
160会话执行一条insert:
insert into p values(2); -- 不执行commit
66会话执行一条insert和160不同的数据:
insert into p values(3); -- 不执行commit
160会话插入一条和66会话相同的数据:
insert into p values(3); -- 因为66会话没有commit,所以此处会卡住等待66会话commit
66会话插入一条和160会话第一次相同的数据:
insert into p values(2); -- 因为160会话没有commit,所以此处会卡住等待160会话commit
此时便形成了死锁,oracle在等待锁的过程中会自动识别出死锁,并在160会话中报出 ORA-00060错误:
insert into p values(3)
*
第 1 行出现错误:
ORA-00060: 等待资源时检测到死锁
即insert into p values(3)
这条sql执行失败。
但是因为160会话的insert into p values(2)
执行成功且没有commit,所以66会话还在继续阻塞等待160会话commit。
160会话commit之后,160会话的insert into p values(2)
会成功写入数据库。
结论
锁定是一个开发的范畴,不是DBA的范畴。
通过锁定,可以达到预期的业务需求。
通过对业务的深入分析,可以最大程度的避免不必要锁定的发生。