前言
平常我们经常听到MySQL的各种锁,会不会却迷茫于他们的关系和各自在MySQL中的作用,今天我们一起来认真学习一下吧~
锁类型
锁介绍
锁粒度
表锁
表锁是MySQL中最大粒度的锁定机制,会锁定整张表,可以很好的避免死锁,是 MySQL 中最大颗粒度的锁定机制。
表锁由 MySQL Server 实现,一般在执行 DDL 语句时会对整个表进行加锁,比如说ALTER TABLE
等操作。在执行 DML 语句时,也可以通过LOCK TABLES
显式指定对某个表进行加锁。
页锁
页级锁是 MySQL 中比较独特的一种锁定级别,在其他数据库管理软件中并不常见。
页级锁的颗粒度介于行级锁与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力同样也是介于上面二者之间。另外,页级锁和行级锁一样,会发生死锁。
页级锁主要应用于 BDB 存储引擎。
行锁
行级锁的锁定颗粒度在 MySQL 中是最小的,只针对操作的当前行进行加锁,所以行级锁发生锁定资源争用的概率也最小。
行级锁主要应用于 InnoDB 存储引擎。
MySQL中3种锁的特性可大致归纳如下:
引擎 | 表锁 | 行锁 | 页锁 |
---|---|---|---|
InnoDB | Yes | Yes | No |
MyISAM | Yes | No | No |
DBD | Yes | No | Yes |
兼容性
共享锁/排它锁
InnoDB实现标准的行级锁定,其中有两种类型的锁, 共享锁(S)
和排它锁(X)
。
- 共享锁允许持有锁,读取行的事务。
- 排它锁允许持有锁,更新或删除行的事务。
-- 加锁方式:共享锁
SELECT...LOCK IN SHARE MODE
-- 加锁方式:排它锁
SELECT...FOR UPDATE
锁模式
记录锁
记录锁(Record Locks)
也称为行锁,顾名思义,表示对某一行记录加锁。
-- id 列为主键列或唯一索引列
SELECT * FROM test WHERE id = 10 FOR UPDATE;
id为1的行记录会被锁住,可以防止从插入,更新或删除行。
记录锁总是锁定索引记录(SELECT和UPDATE都会加锁),即使表没有定义索引。对于这种情况, InnoDB创建一个隐藏的聚集索引并使用该索引进行记录锁定。但因为可能会扫描全表,那么该锁也就会退化为表锁。
注意:
- id列必须为唯一索引或主键列,否则上述语句加的锁会变成临键锁。
- 查询语句必须为精准匹配
=
,不能>
、<
、like
等,否则也会退化成临键锁。
间隙锁
间隙锁(Gap Locks)
是对索引(非唯一索引)记录之间的间隙,锁定一个区间。注意!间隙锁锁住的是一个区间,而不仅仅是这个区间中目前仅存在的数据行。
SELECT * FROM test WHERE id BETWEEN 10 and 15 FOR UPDATE;
例如上面的语句,那(10,15)整个区间的记录行都会被锁住,即id为11,12,13,14数据行的插入操作都会被阻塞,但是10和15两条记录行并不会被锁住。
这里还值得注意的是,不同的事务可以在间隙上持有冲突的锁。例如,事务 A 可以在间隙上持有共享间隙锁(间隙 S 锁),而事务 B 在同一间隙上持有排他间隙锁(间隙 X 锁)。允许冲突间隙锁的原因是,如果从索引中清除记录,则必须合并不同事务在记录上持有的间隙锁。
注意:
- 使用唯一索引搜索唯一行不需要间隙锁定。(这不包括搜索条件仅包含多列唯一索引的某些列的情况;在这种情况下,确实会发生间隙锁定。)
-- id 列为唯一索引列,则不用间隙锁定
SELECT * FROM test WHERE id = 10 FOR UPDATE;
如果id为唯一索引列,则不用间隙锁定。但如果id是普通索引或者未建索引,那么该语句会锁定前面的
间隙。
- 在RC隔离级别下,不会使用间隙锁。只有当隔离级别是RR和Serializable时才会存在间隙锁。
- 间隙锁可以共存。一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。共享和排他间隙锁之间没有区别。它们彼此不冲突,并且执行相同的功能。
临键锁
临键锁(Next-Key)
是记录锁+间隙锁的组合。通过临键锁可以解决幻读的问题。
默认情况下,InnoDB在 REPEATABLE READ
事务隔离级别运行。在这种情况下,InnoDB使用临键锁进行搜索和索引扫描,以防止幻像行,比如select ... in share mode
或者select ... for update
语句。
但即使你的隔离级别是RR,如果你这是使用普通的select语句,那么InnoDB将是快照读,不会使用任何锁,因而还是无法防止幻读。
记住了,锁住的是索引前面的间隙!比如一个索引包含值,10,11,13和20。那么,临键锁的范围如下:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
总结:InnoDB在RR事务隔离级别下,在根据非唯一索引对记录行进行UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE
操作时,InnoDB 会获取该记录行的临键锁 ,并同时获取该记录行下一个区间间隙锁
。
意向锁
意向锁(Intention Locks)
是表级锁,指示事务稍后需要对表中的行使用哪种类型的锁(共享锁或独占锁)。
意向锁有两种类型:
- 意向共享锁(IS):一个事务在获取(任何一行/或者全表)S锁之前,一定会先在所在的表上加IS锁。
- 意向排它锁(IX):一个事务在获取(任何一行/或者全表)X锁之前,一定会先在所在的表上加IX锁。
意图锁定协议如下:
- 在事务可以获取表中行的共享锁之前,它必须首先获取
IS
表上的锁或更强的锁。 - 在事务获得表中行的排他锁之前,它必须首先获得
IX
表的锁。
表级锁类型兼容性总结在以下矩阵中:
请求锁\现有锁 | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果请求事务与现有锁兼容,则向请求事务授予锁,但如果与现有锁冲突,则不会。事务一直等到冲突的现有锁被释放。如果锁定请求与现有锁定发生冲突并且由于会导致死锁而无法授予 ,则会发生错误。
除了全表请求(例如,LOCK TABLES ... WRITE
)之外,意图锁不会阻止任何内容。意图锁的主要目的是表明有人正在锁定一行,或者打算锁定表中的一行。
注意!这里说一下意向锁存在的目的:可以快速判断该表是否存在行锁。假设事务T1,用X锁来锁住了表上的几条记录,那么此时表上存在IX锁,即意向排他锁。那么此时事务T2要进行LOCK TABLE … WRITE
的表级别锁的请求,可以直接根据意向锁是否存在而判断是否有锁冲突。
插入意向锁
插入意向锁(Insert Intention Locks)
是在插入一条记录行前,由 INSERT
操作产生的一种间隙锁
。该锁用以表示插入意向,当多个事务在同一区间插入位置不同的多条数据时,事务之间不需要互相等待。假设存在两条值分别为 4 和 7 的记录,两个不同的事务分别试图插入值为 5 和 6 的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)。
总结来说,插入意向锁的特性可以分成两部分:
- 插入意向锁是一种特殊的
间隙锁
,如果说间隙锁锁住的是一个区间,那么插入意向锁锁住的就是一个点。 - 插入意向锁之间
互不排斥
,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待。
需要强调的是,虽然插入意向锁中含有意向锁三个字,但是它并不属于意向锁而属于间隙锁,因为意向锁是表锁而插入意向锁是行锁。
自增锁
自增锁(auto-inc Locks)
是一种特殊的表级锁,主要用于事务中插入自增字段,也就是我们最常用的自增主键id。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务都必须等待自己插入到该表中,以便第一个事务插入的行接收连续的主键值。
可以通过配置项 innodb_autoinc_lock_mode
调整自增锁算法。
锁示例分析
数据表状态
表结构:
CREATE TABLE `test1` (
`id` int(20) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
表初始化数据:
mysql> select * from test1;
+----+--------+
| id | name |
+----+--------+
| 1 | user1 |
| 5 | user5 |
| 10 | user10 |
| 15 | user15 |
| 20 | user20 |
| 25 | user25 |
+----+--------+
6 rows in set (0.00 sec)
示例1
时间 | 事务A | 事务B |
---|---|---|
T1 | begin; | |
T2 | begin; | |
T3 | select * from test1 where id=11 for update;先请求IX锁并成功获取;再请求X锁,但因行记录不存在,故得到的是间隙锁(10,15) | |
T4 | select * from test1 where id=12 for update;先请求IX锁并成功获取;再请求X锁,但因行记录不存在,故得到的是间隙锁(10,15) | |
T5 | insert into test1(id,name) values(11, "user11");请求插入意向锁(11);因事务B已有间隙锁,请求只能等待 | |
T6 | 锁等待中 | insert into test1(id,name) values(12, "user12");请求插入意向锁(12);因事务A已有间隙锁,请求只能等待 |
T7 | 事务B被回滚,事务A立刻获得锁,插入成功 | 死锁,事务B被回滚 |
事务B插入时报错:
解释:
在场景一中,因为IX锁是表锁且IX锁之间是兼容的,因而事务一和事务二都能同时获取到IX锁和间隙锁。
另外,需要说明的是,因为我们的隔离级别是RR,且在请求X锁的时候,查询的对应记录都不存在,因而返回的都是间隙锁。
示例2
时间 | 事务A | 事务B |
---|---|---|
T1 | begin; | |
T2 | begin; | |
T3 | select * from test1 where id=11 for update;先请求IX锁并成功获取;再请求X锁,但因行记录不存在,故得到的是间隙锁(10,15) | |
T4 | select * from test1 where id=16 for update;先请求IX锁并成功获取;再请求X锁,但因行记录不存在,故得到的是间隙锁(15,20) | |
T5 | insert into test1(id,name) values(11, "user11");请求插入意向锁(11),获取成功 | |
T6 | commit; | insert into test1(id,name) values(16, "user16");请求插入意向锁(12),获取成功 |
T7 | commit; |
解释:
两个间隙锁没有交集,而各自获取的插入意向锁也不是同一个点,没有冲突,因而都能执行成功。
总结
通过本次学习,我们已经对MySQL锁的分类已经有了一个大致的认识。以后我们继续在实践中思考和发现不同的语句到底都加了哪些锁,是否会造成死锁等。