一、锁的类型
Mysql可以按照粒度划分为表锁和行锁
- 表锁范围大,上锁慢,并发低
- 行锁仅锁索引,上锁慢,并发高
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。
而行锁又分为排他锁和共享锁
- 共享锁(S):允许获得该锁的事务读取数据行(读锁),同时允许其他事务获得该数据行上的共享锁,并且阻止其他事务获得数据行上的排他锁。
- 排他锁(X):允许获得该锁的事务更新或删除数据行(写锁),同时阻止其他事务取得该数据行上的共享锁和排他锁。
表锁和行锁同时存在就会带来一个问题,如果要申请表锁的时候如何确定当前表没有行锁呢?简单的方式当然是遍历查找,但这样效率太低,所以引入了“意向锁”(intention lock)。在添加行锁之前对表添加意向锁。
slideshare上对意向锁的一段表述:
IS and IX locks allow access by multiple clients. They won’t necessarily conflict until they try to get real locks on the same rows. But a table lock (ALTER TABLE, DROP TABLE, LOCK TABLES) blocks both IS and IX, and vice-versa.
翻译成中文:
IX(意向排他锁),IS(意向共享锁)是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突
除了上面提到的锁,还有一种,叫“间隙锁”。
二、间隙锁(Gap Locks)
什么是间隙锁
先来看看Mysql官网上的定义:
A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.
翻译过来就是:间隙锁(Gap Lock)锁定的是索引记录之间的间隙、第一个索引之前的间隙或者最后一个索引之后的间隙。
比如,当前数据库存在这样的一张person表,id是主键,age是普通索引,即二级索引:
id | name | age |
---|---|---|
1 | 张三 | 13 |
3 | 李四 | 14 |
4 | 王五 | 16 |
在事务A中有一项操作是查询id在1~3范围内的数据:
...
select * from person where id between 1 and 3
...
显而易见,应该返回张三
和李四
这两条数据。
但如果在事务A尚未提交时,此时另一个事务B插入一条id为2的数据,并提交成功,那么当事务A结束后,会发现查询出来的数据(2条)少于数据库当前存在的数据数量(3条),这就产生了幻读的问题。
所以InnoDB引入间隙锁,来锁住1到3中所有的数据,这样不会有其他事务影响当前事务的范围操作。
这就是间隙锁的主要、唯一目的:阻止其他事务在间隙上插入记录
间隙锁的触发时机
- REPEATABLE READ或以上的隔离级别下
- locking reads,UPDATE和DELETE时,除了对唯一索引的唯一搜索外,都会获取gap锁或next-key锁。即锁住其扫描的范围。
为什么要锁住“第一个索引之前的间隙或者最后一个索引之后的间隙”呢?
再举个例子:
select * from person where age = '14' for update
这时如果另一个事务执行如下操作,会发现执行失败:
insert into person values(2, '赵六', 14)
这就引出另一个问题:数据间隙的分析时,优先通过二级索引排序,再通过二级索引中存储的主键排序
由于二级索引存放在B+树上,B+树的特点是不允许值重复,所以需要让主键共同参与二级索引值的构成来保证唯一性。
二级索引指向主键而不是数据地址,这样防止行移动或者数据页分裂时导致的二级索引维护工作。
所以,你的期望是锁住age为14的数据,但数据库不知道你要锁住的是14和哪个主键构成的二级索引,所以只能把id在1~3范围内的数据都锁住。
另外其间隙锁的区间是(1, 3]
,即大于1小于等于3。
Next-key 锁
相当于一个索引记录锁加上该记录之前的一个间隙锁。Next-key锁和MVCC共同作用下解决了幻读的问题。
三、Mysql数据上锁方式
1. 被动锁
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁,即快照读
2. 主动锁
-
FOR UPDATE
增加排他锁,即当前读,每次执行都会生成一个新的读视图
-
FOR SHARE
增加共享锁