MySQL
在REPEATABLE READ
级别解决了幻读问题,解决方案有两种,一种是MVCC
版本控制链,具体可以参考这个,MVCC 多版本控制链,还有就是通过加锁的方式。这篇文章简要介绍一下MySQL
是如何通过加锁来解决幻读问题的。
准备工作
还是一样,先创建一张表,如下所示:
CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;
然后插入如下数据:
INSERT INTO hero VALUES
(1, '刘备', '蜀'),
(3, '诸葛亮', '蜀'),
(8, '曹操', '魏'),
(15, '荀彧', '魏'),
(20, '孙权', '吴');
此时MySQL
中的示意图简化如下所示:
前置知识
MySQL
中行锁分为两种,共享锁S
,以及独占锁X
。当使用select * from ...
或者是select * from ... in share mode
这两种语法时,会对进行搜索的行记录加上S
锁。当使用select * from ... for update
这个语法时就会对记录加X
锁。
当一个事务获取了一条记录的S
锁,其他事务可以获取该记录的S
锁,但不可以获取X
锁;当一个事务获取了一条记录的X
锁,其他事务不可以继续获取该条记录的X
锁或者S
锁。
分析
Record Locks
当开启一个事务,使用select * from
语句时,InnoDB
会对搜索的记录进行加锁,官方名称为LOCK_REC_NOT_GAP
,具体是S
锁还是X
锁,具体视select
语句而定。例如现在查找number
值为8的记录,
select * from hero where number=8;
此时示意图如下所示:
Gap Locks
接下来就涉及到了MySQL
如何利用加锁来解决REPEATABLE READ
级别解决幻读了。上面说了,当使用select
语句查询时,InnoDB
会对记录加记录锁,但这就出现一个问题了,这个事务执行第一次查询时,另一个事务要插入的数据此时还不存在,这个事务就无法对这些幻影记录加上记录锁啊,那该怎么办呢?为了解决这个问题,InnoDB
引入了一种新的锁机制,称之为LOCK_GAP
,间隙锁。例如,我们可以在number
值为8的那条记录上添加一个gap
锁,如下所示:
当一条记录被加上gap
锁时,就意味着不允许其他事务在number
值8的前面的间隙,即(3,8)这个区间内插入新的记录。比如说,现在有另一个事务想要插入一条记录,(4,"张飞",“蜀”)。此时,定位到该条记录的下一条记录的number
值为8,而这条记录上又有一个gap
锁,因此这个插入操作就会被阻塞住,直到拥有这个gap
锁的事务提交之后,number
列的值在(3,8)中的新记录才能被插入。
gap
锁的主要作用就是为了防止插入幻影记录,如果你对一条记录加了gap
锁,并不会限制其他事务对这条记录继续加记录锁或者gap
锁。
Next-Key Locks
当我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新纪录,此时该怎么办呢?这个时候,InnoDB
又出现了一个新的锁结构,LOCK_ORDINARY
,也被称之为next-key
锁,临键锁。它其实是record
锁和gap
锁的结合体。如下所示,给number
值为8的记录加一个next-key
锁。
总结
InnoDB
中存在着不同的锁,当使用select
语句查询某条记录时,InnoDB
会对该记录加记录锁,即record
锁;- 当一个事务对某条记录加上
gap
锁时,另一个事务此时向这条记录前面间隙插入新记录的操作将会被阻塞; next-key
锁,又称为临键锁,是记录锁和间隙锁的结合体。既锁住某条记录,又会将该记录前面的间隙一同锁住,解决了MySQL
在REPEATABLE READ
级别下的幻读问题。