1 关于锁(what)
1.1 什么是锁
锁是用来限制多用户(线程/进程)对共享资源的访问和使用而采用的一种技术手段。CPU、IO、数据都是共享资源,如何保证共享资源使用的一致性、有效性及高效性是必须解决的一个问题(还有一个需要平衡的问题:一致性提高了,高效性(并发)就会降低),为了解决这个问题,数据库系统中引入了事务的概念。
请大家思考:如果数据库不支持事务会存在什么问题?关于这个问题及事务相关的内容参见我的另一篇博文:The transaction of mysql
1.2 锁的分类
不同数据库有不同的锁实现,mysql中的锁根据锁的粒度来区分,有表锁、页面锁及行锁。三种锁在加锁开销、加锁速度、锁的粒度及写并发等方面的对比参见下表:
锁类型 | 锁的目标资源 | 开销 | 加锁速度 | 粒度 | 写并发 |
---|---|---|---|---|---|
表锁 | 表 | 小 | 快 | 大 | 低 |
页 | 页锁 | 中 | 中 | 中 | 中 |
行锁 | 索引 | 大 | 慢 | 小 | 高 |
各种锁的特性不同,其各自的应用场景便不同,抛开背景谈方案都是耍流氓,没有可以解决一切问题的银弹!
mysql的innodb引擎采用了表锁及行锁两种方式,本文则重点讲解innodb引擎的锁。
由于行锁的锁定目标是索引,所以如果你的语句如果不是根据索引来进行定位记录,那么innodb将锁定整个表!
1.3 Innodb的行锁
本着大处着眼,小处着手的原则,我们先通过一个脑图来整体认识下innodb的锁:
共享锁与排它锁
共享锁与排它锁属于行锁,锁定目标为索引,他们的描述如下:
- 共享锁(S): 允许获取S锁的事务读取一行记录
- 排它锁(X): 允许获取X锁的事务更新、删除该记录
如果事务T1持有记录r1的共享锁,此时T2也来获取r1的锁时会发生:
- T2获取r1的共享锁时,可以立即得到。此时T1与T2均持有记录r1的共享锁;
- T2获取r1的排他锁时,T2将进入等待状态;
获取共享锁:
SELECT * FROM TABLE LOCK IN SHARE MODE;
获取排他锁:
SELECT * FROM TABLE FOR UPDATE;
UPDATE TABLE SET ...;
共享&排他意向锁
为了使行锁与表锁共存,innodb还使用了意向锁(意向锁的意义?)。意向锁属于表锁,其锁定目标为表,它表示一个事务即将(期望)对表中的某条记录加共享锁或排它锁,它们的描述如下:
- 意向共享锁(IS):获取该锁的事务T期望对表t中的记录加S锁
- 意向排他锁(XS):获取该锁的事务T期望对表t中的记录加X锁
获取意向共享锁:SELECT * FROM TABLE LOCK IN SHARE MODE;
获取意向排他锁:SELECT * FROM TABLE FOR UPDATE;
意向排他锁的规则:
- 在事务T获取表t的记录r的共享锁S前,事务T必须首先获取表t上的IS锁或更强的锁(IX);
- 在事务T获取表t的记录r的排它锁X前,事务T必须首先获取表t上的IX锁;
当一个事务T1获取了A锁时,事务T2期望获取B锁时的兼容矩阵为:
A\B | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
注:
- 意向锁为表级锁,但表示的是事务正在操作某一行记录。
- 意向锁之间不会发生冲突。
思考分析:
- 事务T1获取了表t中的记录r1的X锁,在此情况下,事务T2尝试获取表t中记录r1的X锁会怎样?请分析出T1及T2的持锁过程与结果
锁定过程如下表:
|T1|T2
:-:|:-:|:-:
1|SET AUTOCOMMIT=0;BEGIN;|SET AUTOCOMMIT=0;BEGIN;
2|GET TABLE IX LOCK ON t|
3|GET RECORD X LOGCK ON t.r1|
4||GET TABLE IX LOCK ON t
5||GET RECORD X LOCK ON t.r1
事务T2在执行到第5步时会进入等待状态
持锁结果如下表:
事务 | 持有的锁 | 等待的锁 |
---|---|---|
T1 | IX、X LOCK ON RECORD t.r | |
T2 | IX | X LOCK ON RECORD t.r |
- 事务T1获取了表t中的记录r1的X锁,在此情况下,事务T2尝试获取表t中记录r2的X锁会怎样?请分析出T1及T2的持锁过程与结果
- 事务T1获取了表t中的记录r1的X锁,在此情况下,事务T2尝试获取表t的X锁会怎样?请分析出T1及T2的持锁过程与结果
记录锁
记录锁: 仅仅锁住索引记录的一行。单条索引记录上加锁,record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
区间锁(GAP锁):仅仅锁住一个索引区间(开区间)。在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身:(A,B),GAP锁之间互相不冲突。
NETX-LOCK锁:record lock + gap lock, 左开右闭区间:(A,B]
插入意向锁:插入意向锁是一种特殊的间隙锁(GAP LOCK)。它表明事务T期望想表t中的某个间隙插入一条记录。
插入意向锁(II):获取该锁的事务T期望向表t中插入新记录;事务T在向表t中插入记录r前,必须首先获取插入意向锁。在插入时,如果发生duplicate-key error,则事务会立刻获取一个记录共享锁。
思考分析:
1 请分析下面的情况
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
|T1|T2|T3
:-:|:-:|:-:|:-:
1|SET AUTOCOMMIT=0;BEGIN;|SET AUTOCOMMIT=0;BEGIN;|SET AUTOCOMMIT=0;BEGIN;
2|insert into table t values(1)||
3||insert into table t values(1)|
4|||insert into table t values(1)
5|rollback||
6||?|?
2 请分析下面的情况
|T1|T2|T3
:-:|:-:|:-:|:-:
1|SET AUTOCOMMIT=0;BEGIN;|SET AUTOCOMMIT=0;BEGIN;|SET AUTOCOMMIT=0;BEGIN;
2|delete from t1 where i = 1;||
3||insert into table t values(1)|
4|||insert into table t values(1)
5|commit||
6||?|?
行锁的兼容矩阵为:
- | GAP | Insert Intention | Record | Next-Key |
---|---|---|---|---|
GAP | 兼容 | 兼容 | 兼容 | 兼容 |
Insert Intention | 冲突 | 兼容 | 兼容 | 冲突 |
Record | 兼容 | 兼容 | 冲突 | 冲突 |
Next-Key | 兼容 | 兼容 | 冲突 | 冲突 |
表注:横向是已经持有的锁,纵向是正在请求的锁。
由于S锁和S锁是完全兼容的,因此在判别兼容性时只考虑持有的锁与请求的锁是这三种组合情形:X、S和S、X和X、X。
另外,需要提醒注意的是进行兼容判断也只是针对于加锁涉及的行有交集的情形。
分析兼容矩阵可以得出如下几个结论:
- INSERT操作之间不会有冲突。
- GAP,Next-Key会阻止Insert。
- GAP和Record,Next-Key不会冲突
- Record和Record、Next-Key之间相互冲突。
- 已有的Insert锁不阻止任何准备加的锁。
2 SQL语句与加锁
规则1:默认情形下,所有满足条件的记录都加next-key锁,第一个不满足条件的记录也加next-key锁。
规则2:在规则1的前提下,规则2可以视为对规则1的优化
条件1 | 条件2 | 规则 |
---|---|---|
等值条件 | unique扫描 | 不存在该记录加gap锁 |
等值条件 | unique扫描 | 存在该记录且该记录未被删除,加not gap锁 |
等值条件 | unique扫描 | 存在该记录但该记录已被删除,加next-key锁 |
等值条件 | 非unique扫描 | 所有满足条件的记录都加next-key锁,第一个不满足条件的记录加gap锁 |
规则 | 向后扫描时,所有满足条件的记录都加next-key锁,第一个满足条件的下个记录加gap lock锁 | |
规则 | Infimum record永远不加锁,Supremum record永远加gap lock |
链接:https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html