锁的分类

没有并发就没有锁

锁分类:

  • Enqueues:队列类型的锁,通常和业务的DML操作相关。平时说Lock锁时通常指的是Enqueues。
  • Latches:系统资源方面的锁,比如内存结构、SQL解析…

锁的原则

只有被修改时,行才会被锁定。

当一条语句修改了一条记录,只有这条记录上被锁定,在Oracle数据库中不存在锁升级。

有些品牌的数据库,如果一个表中有很多行都加了行锁,就会升级为对这个表的表锁。Oracle没有锁升级的策略。

当某行被修改时,它将阻塞别人对它的修改。

当一个事务修改一行时,将在这个行上加上行锁(TX),用于阻止其他事务对相同行的修改。

读永远不会阻止写。

读不会阻塞写,但是有唯一的一个例外,就是:select ... for update

写永远不会阻塞读。

当一行被修改后,Oracle通过回滚段提供给数据的一致性读。

锁的类型

通过视图v$lock_type查询所有锁的类型。

  1. select * from v$lock_type;

主要用到的是TM锁和TX锁:

  • TM表锁:发生在 insert、update、delete以及 select for update操作时,目的是保证操作能够正常进行,并且阻止其他人对表执行DDL操作。
  • TX事务锁(行锁):对于正在修改的数据,阻止其他会话进行修改。

在执行一条update语句时,只有被修改的那一行上面会加一个TX锁,而在被操作的整张表上会加一个TM锁。

锁的阻塞

环境搭建:

  1. create table t(id int primary key);
  2. insert into t values(1);
  3. insert into t values(2);

update操作

通过v$lock查看数据库的锁:

第一个用户:

  1. select distinct sid from v$mystat; -- 查看当前会话的SID,例如 160
  2. update t set id=3 where id=2; -- 执行一条update语句,但是不提交

第二个用户:

  1. select distinct sid from v$mystat; -- 查看当前会话SID,例如66
  2. update t set id=1 where id=2; -- 因为160会话没有进行commit,所以该会话执行update时卡住不动

通过v$lock表查看数据库的锁:

  1. 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中的编号:

  1. select * from dba_objects where object_id='93015';

TX锁的 ID1 和 ID2 经过位运算,得到的是该TX锁定的回滚段上的位置。

通过v$session_wait查看会话的锁状态:

  1. 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操作:

  1. insert into t values(1); -- insert之后没有进行commit提交

66会话执行insert操作:

  1. insert into t values(2);
  2. commit; -- 插入一条和160会话不冲突的数据,可以正常的插入并提交
  3. insert into t values(1); -- 插入一条河160会话相同主键的数据,因为160会话没有提交,所以此处会卡住

此时,查看v$lock中对应的锁:

  1. 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操作:

  1. select * from t where id=2 for update; -- 此时不进行commit

66会话执行update操作:

  1. 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模式的锁:

  1. lock table in share mode; -- 没有commit,此时160会话会为t表添加一个4类型锁,其他会话只允许select操作

66会话此时执行insert:

  1. insert into t values(6); -- 因为160会话添加了锁,所以66会话的insert会卡住

此时查看v$lock表:

  1. 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一条数据:

  1. lock table in share mode;
  2. insert into t values(5); -- 此时没有commit;

66会话insert一条河160会话不同的数据:

  1. insert into t values(6); -- insert的数据和160会话不同,但是因为160会话手工加了4模式锁,所以此处也会卡住

此时查看v$lock表:

  1. 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模式锁。

手工锁定表的应用:

  1. lock table dept in share mode; -- 后面几条sql都需要用到dept表,防止几条sql执行过程中dept表被其他用户修改,此处手工将dept表锁定,防止被其他用户修改
  2. update emp set sal=sal*1.1 where deptno in (select deptno from dept where loc='DALLAS');
  3. update budget set totsal=totsal*1.1 where deptno in (select deptno from dept where loc='DALLAS');
  4. commit; -- 释放锁

RI锁

基于外键引用关系的锁定

当对具有主外键关系的表做DML操作时,锁定不单单发生在操作表上,相应的引用表上也可能加上相应的锁定。

环境搭建:

  1. create table p(id int primary key); -- 主表
  2. create table c(id references p(id)); -- 从表,外键指向p表的id

向p表插入一条数据:

  1. insert into p values(1); -- 向主表插入一条数据,不进行commit;

查看v$lock视图:

  1. 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表的数据:

  1. update t set id=2 where id=1; -- 更新主表的一条数据,不进行commit;

查看v$lock视图:

  1. 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会话向主表插入一条数据:

  1. insert into p values(1); -- 不进行commit;

66会话向从表插入一条数据:

  1. insert into c values(1); -- c中插入的数据需要引入p表的id,因为160会话还没有提交,所以p表是否有id=1这条数据还不确定。此处会卡住

查看v$lock

  1. 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:

  1. insert into p values(2); -- 不执行commit

66会话执行一条insert和160不同的数据:

  1. insert into p values(3); -- 不执行commit

160会话插入一条和66会话相同的数据:

  1. insert into p values(3); -- 因为66会话没有commit,所以此处会卡住等待66会话commit

66会话插入一条和160会话第一次相同的数据:

  1. insert into p values(2); -- 因为160会话没有commit,所以此处会卡住等待160会话commit

此时便形成了死锁,oracle在等待锁的过程中会自动识别出死锁,并在160会话中报出 ORA-00060错误:

  1. insert into p values(3)
  2. *
  3. 1 行出现错误:
  4. 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的范畴。

通过锁定,可以达到预期的业务需求。

通过对业务的深入分析,可以最大程度的避免不必要锁定的发生。