InnoDB的行锁包括:记录锁、间隙锁(Gap Lock,解决幻读问题)和组合锁。
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
顾名思义,就是给一行数据记录上锁。
注意:以下所有例子都是默认在RR隔离级别下:
// 创建表 t
mysql> CREATE TABLE `t`(
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
)ENGINE=InnoDB;
// 插入记录
mysql> INSERT INTO t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),
(20,20,20),(25,25,25);
假设,只给d=5这条记录上锁,执行以下语句:
mysql> select * from t where d=5 for update;
幻读
解决:
幻读产生的原因是,行锁只能锁住行,而插入语句是要插入到现有数据记录的“间隙”。因此,为了解决幻读,InnoDB引入了间隙锁。
间隙就是两个值之间的间隙,如开头创建的表t,插入了6条记录,则产生7个间隙,都是开区间,如下图:
分析:select * from t where d=5 for update;
实际上,因为d上没有索引,所以会走全表扫描,在一行一行的扫描过程中,不仅给每行加上锁,而且给两个值之间的间隙也加了间隙锁,这样就可以确保无法插入新的数据记录。
就是间隙锁+行锁,前开后闭区间。如:(5,10], (10, 15],(15, 20],(20, 25]。
分析:
分析:这里 session A 要给索引 c 上 c=5 的这一行加上读锁。
注意,这个例子中是以 lock in share mode 方式加锁,这种加锁方式默认不会修改数据,只会锁覆盖索引。如果是以 for update 方式,系统会认为你接下来会修改数据,顺便就主键索引上满足条件的行加上行锁。
例三:主键索引范围锁
先来看看,以下两条语句有什么不同吗?
mysql> select * from t where id=10 for update;
mysql> select * from t where id>=10 and id<11 for update;
对于整型字段来说,上述两条语句在逻辑上执行结果是一致的,但是加锁的范围却不同。下面,我们来看看第二条语句的加锁效果:
分析:
所以,Session A这时候的锁范围就在主键索引id=10的行锁,以及(10,15]的next-key lock;这样SessionB 和 C的执行结果你就理解了。
例四:非唯一索引范围锁
与例子三不同的是,用的是非唯一索引c。
select * from t where c>=10 and c<11 for update;
所以,Session A就加了(5,10] 和 (10,15] 的next-key lock; 那么此时主键索引上有那些锁呢?我们继续来验证:
可以看到,主键id=10的记录被加了锁。
例五:唯一索引范围锁Bug
mysql> select * from t where id >10 and id <=15 for update;
分析:
所以,Session B 对于id=20行记录的update会被阻塞,同样,Session C的插入语句也会被阻塞。
例六:非唯一索引上的“等值”的例子
先给表t插入一行记录
mysql> insert into t values(30, 10, 30);
新插入的行数据c=10,此时表中就有两条c=10 的记录,分别是(10,10,10)和 (30,10,30)。但是,这两条记录的主键值不同,也就是说,这两个c=10的记录中间也有间隙。
分析:
所以,Session B被阻塞,Session C执行成功。加锁范围如下:
例七:limit语句
分析:
虽然,你知道只有两条满足条件的记录,两个语句的执行逻辑是一致的,但是与例六不同的是,加了limit 2限定,在找到两条c=10的记录后,就不再往后扫描了,所以加锁范围也有了不同,如下:
很好的说明了,next-key lock等价于 “行锁 + 间隙锁”。实际上,Session B的加锁是分为两步的,先加(5,10)的间隙锁,间隙锁和间隙锁不冲突,添加间隙锁成功,再去申请行锁的时候,阻塞了。从而导致了Session A 往这个间隙的插入操作阻塞了,形成死锁,InnoDB 让事务B回滚。
可重复读隔离级别下,普通的查询(select … from t where …;)是快照读,不会加锁,也就是利用MVCC读取数据。
当前读:select…for update 是当前读,当前读就是能读到所有已提交的记录的最新值。
select … where…lock in share mode,这种方式会加S共享锁
select…where…for update,这种方式会加X排它锁,
如果是update、delete都会加X排他锁,具体这个锁是加在聚簇索引还是二级索引,以及对应索引的哪些记录上,由过滤条件:1.是哪种索引(主键索引、唯一索引、非唯一索引);2.事务的隔离级别来决定。
笔记参考于极客时间《MySQL实战45讲》