对于开发过并发编程的同学都知道,对于公共资源的管理,为了确保每个用户能以一致性的方式读取和修改数据,锁是必不可少的机制。inoodb由锁机制和前面说到的MVCC共同保证了事务的隔离性。
在数据库中,锁分为lock和latch两种,innodb的latch分为mutex(互斥量)和rwlock(读写锁),是用来保证并发线程临界资源的正确性的,简单的说latch主要作用于线程,目标对象是内存数据结构、线程的临界资源等;而lock便是我们本篇要说的锁,作用于事务,目标对象是数据库中的表、页、行等。
MySQL把innodb的锁分成了8类:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html,英语较好的同学可以直击官网,看看官方对这8种锁的的解读,下面我也按自己的理解对官方的文档进行解析(想什么呢,肯定有官网没提到的干货鸭):
-
Shared and Exclusive Locks(共享锁和独占锁,行锁)
-
Intention Locks(意向锁,表锁)
-
Record Locks(记录锁,锁算法)
-
Gap Locks(间隙锁,锁算法)
-
Next-Key Locks(区间锁,锁算法)
-
Insert Intention Locks(插入意向锁)
-
AUTO-INC Locks(自增锁,表锁)
-
Predicate Locks for Spatial Indexes(用于空间索引上的锁,空间索引与传统B+树索引不同,用的很少)
Shared and Exclusive Locks
InnoDB实现了标准的行级锁定:
共享锁(S):共享锁允许持有该锁的事务读取一行
独占锁(X):也叫排他锁,允许持有锁的事务更新或删除行
比方说事务T1获取了t表r行的S锁,事务T2也可以获得r行的S锁,而事务T3要获取r行的X锁的话要等待T1和T2释放S锁。T3获得了r行的X锁后,其它任务事务要获取r行的S锁或X锁都要等待T3将X锁释放。
对r行加锁的方式有一致性锁定读:
select ... from table ... lock in share mode; -- 添加S锁
select ... from table ... for update; -- 添加X锁
对r行加X锁的方式还可以直接执行update语句和delete语句。
Intention Locks
意向锁是表级锁,用于指示事务稍后对表中的行需要哪种类型的锁(共享或独占)。有两种类型的意向锁:
意向共享锁(IS)表示事务想要获取表中某几行的共享锁,加S锁前innodb会对表加IS锁
意向独占锁(IX)表示事务想要获取表中某几行的排他锁,加X锁前innodb会对表加IX锁
意向锁被设计出来的的主要意图是当事务要对表加读写锁的时候(如MySQL server层的表锁lock tables 表名 read/write;),不用全表搜索查看哪行是否有被加锁,提高效率。对于表锁的使用场景,innodb没有定义意向锁外的其它读写表锁,MySQL server层有表锁命令lock tables 表名 read/write;至于网上很多博文说索引失效的时候MySQL会放弃行锁使用表锁,我特地试了试不使用索引加X锁的情况,发现没有走任何索引时,那么将会在每一条聚集索引后面加X锁,类似于表锁,但是它们意义是完全不同的,它的锁等待与意向锁无关,发生在给被锁定记录加锁的时候,下面给出实验记录:
事务A | 事务B |
---|---|
set autocommit=0; begin; | set autocommit=0; begin; |
select * from demo1 where id_=5 for update; | |
select * from demo1 where age_=10 for update; 这里age_字段没加索引要给所有聚集索引加X锁 在id_=5记录加锁时阻塞锁等待 | |
select * from demo1 where id_=6 for update; -- 正常执行 | |
select * from demo1 where id_=4 for update; -- 阻塞锁等待 |
以上表demo1‘id_’为主键,‘age_’为普通字段,可以发现id_<4的行记录其实都被事务B锁了,当事务B要锁id_=5的行记录时发生了锁等待,当事务A要给id_=4的记录加锁时,产生了死锁。以下是事务B锁等待时的show engine innodb status的输出信息:
select * from demo1 where v2='v22' for update
------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 242 page no 3 n bits 88 index PRIMARY of table `demo`.`demo1` trx id 140072 lock_mode X waiting
Record lock, heap no 15 PHYSICAL RECORD: n_fields 8; compact format; info bits 0
0: len 8; hex 8000000000000006; asc ;;
1: len 6; hex 000000021c36; asc 6;;
2: len 7; hex 36000002352b7b; asc 6 5+{;;
3: len 2; hex 7631; asc v1;;
4: len 2; hex 7632; asc v2;;
5: len 2; hex 7633; asc v3;;
6: len 2; hex 7634; asc v4;;
7: len 8; hex 0000000000000007; asc ;;
下面是官网给出的表级锁兼容性视图:
X | IX | S | IS | |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
IX | Conflict | Compatible | Conflict | Compatible |
S | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
我们可以使用命令show engine innodb status来查看数据库中意向锁的相关内容:
innodb的三种行锁算法
Inndob存储引擎有3种行锁算法:
Record Lock:锁定单个索引记录,若表没有建立索引,则锁定隐藏字段rowid
Gap Lock:间隙锁,锁定一个范围,但不包括记录本身,间隙锁间是不会有冲突的,它仅仅阻止其它事务插入间隙
Next-Key Lock(Record Lock + Gap Lock):区间锁,锁定一个范围,而且包括本身,例如索引列a的值有1、3、6、9,间隙锁的取值范围就有(-∞, 1], (1, 3], (3, 6], (6, 9], (9, +∞) 。当查询拥有唯一索引如where id_=100时,为了提高效率节省资源,Next-Key Lock会退化为Record Lock。
区间锁的出现,主要是为了解决幻读问题,使innodb的隔离级别Repeatable Read以更小的代价,达到和Serializable几乎一样的效果。这也能想通为何Next-Key Lock会退化为Record Lock,因为查询时锁定了唯一索引行,根本不可能有幻读产生。(提前科普:隔离级别Repeatable Read有使用区间锁算法;幻读:不加表锁的情况,事务A执行select count(0) from table where id>5;事务B此时插入新记录,事务A再次查询发现结果不一样了;有经验的同学可能会认为前篇说的MVCC能完美的解决幻读,不过事实上MVCC是有失效的情况的,幻读和隔离级别后面再详细介绍吧,这里先有一个概念就好)
下面举个例子,清晰的看看区间锁是什么个样子的:
1、建表demo3 ,这里将age_设定为varchar类型防止你出现间隙锁只对number类型有效的错觉
CREATE TABLE `demo3` (
`id_` INT(11) NOT NULL AUTO_INCREMENT,
`age_` VARCHAR(8) NULL DEFAULT NULL,
PRIMARY KEY (`id_`),
INDEX `age_` (`age_`)
)
2、插入测试数据
3、开始事务A和事务B,隔离级别为Repeatable Read
事务A | 事务B |
---|---|
set autocommit=0; begin; | set autocommit=0; begin; |
select * from demo3 where age_=3 for update; 这里age_是辅助索引,所以使用区间锁,聚集索引Record Lock 辅助索引加Gap Lock,故id_=5加X锁,age_(1,3)(3,6)加间隙锁 | |
select * from demo3 where age_=4 for update; -- 正常执行 这里说明间隙锁之间是不会冲突的 | |
insert into demo3 select 6,7; -- 正常执行 不在间隙锁范围,可以插入 | |
insert into demo3 select 8,4; -- 阻塞,age_=4命中间隙锁,锁等待 |
Insert Intention Locks
插入意图锁是在插入行之前由插入操作设置的一种间隙锁。此锁表示插入的意图,如果插入到同一索引间隙中的多个事务没有插入到间隙中的同一位置,则它们不需要等待对方。假设存在值为4和7的索引记录。分别尝试插入值5和6的单独事务,在获得插入行的独占锁之前,每个事务都使用插入意图锁锁定4和7之间的间隙,但不会相互阻止,因为这些行不冲突。看官网的案例,感觉和前面描述的间隙没啥区别,下面贴出官网案例:
1、建表,并插入两条数据
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);
2、启动事务锁定大于100的记录
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
3、启动事务插入记录101,被阻塞
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` trx id 8731 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000066; asc f;;
1: len 6; hex 000000002215; asc " ;;
2: len 7; hex 9000000172011c; asc r ;;...
AUTO-INC Locks
AUTO-INC锁是一种特殊的表级锁,由插入到具有AUTO_增量列的表中的事务使用。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务都必须等待自己向该表中插入值,以便第一个事务插入的行接收连续的主键值。
经测试,在事务种不会与其它任何锁有冲突,应该只是为了保证自增列唯一且顺序而存在的:
事务A | 事务B |
---|---|
set autocommit=0; begin; | set autocommit=0; begin; |
select * from demo3 where id_=5 for update; -- 验证自增锁与IX锁是否冲突 | |
insert into demo3 (age_) values ('15'); -- 验证不同事务插入操作是否阻塞 | |
insert into demo3 (age_) values ('16'); -- 正常执行 |
Predicate Locks for Spatial Indexes
从MySQL 5.7.4 LAB版本开始,innodb开始支持空间索引Spatial Indexes,要处理涉及空间索引的操作的锁定,Next-Key Lock无法很好地支持可重复读取或可序列化事务隔离级别。多维数据中没有绝对的排序概念,因此不清楚哪个是“Next-Key”键。
为了支持具有空间索引的表的隔离级别,InnoDB使用Predicate Lock。空间索引包含最小边界矩形(MBR)值,因此InnoDB通过对用于查询的MBR值设置Predicate Lock来强制索引的一致读取,使其他事务无法插入或修改与查询条件匹配的行。
写着写着发现本篇篇幅有些长了,哈哈,不知道你晕了没有,晕了就自行拆开慢慢看。最后,如有错误,敬请斧正;欢迎转载,但请务必注明出处;最后,在此向神奇的海螺保证,绝不太监!!!