《MySQL——幻读与next-key lock与间隙锁带来的死锁》

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;

insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

该表除了主键id,还有索引c。

问下面的语句序列,是怎么加锁的,加的锁又是什么时候释放的呢?

begin;
select * from t where d = 5 for update;
commit;

这条语句会命中d=5这一行,对应主键id=5,因此在select语句执行完成后,id=5这一行会加一个写锁,并且由于两阶段锁协议,这个写锁会在执行commit语句的时候释放。

由于字段d上没有索引,因此这条查询语句会做全表扫描,那么,其他被扫描的不满足的行记录会不会被加锁?

幻读现象

如果旨在id=5这一行加锁,而其他行不加锁,在下面这个情况下:
在这里插入图片描述
session A执行了三次当前读,并且加上了写锁。

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

幻读与不可重复读的区别

在同一个事务中,两次读取到的数据不一致的情况称为幻读和不可重复读。幻读是针对insert导致的数据不一致,不可重复读是针对 delete、update导致的数据不一致。

1、在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。

2、session B的修改结果被session A之后的select语句用当前读看到,不能称为幻读。幻读仅仅指"新插入的行"

幻读带来的问题

1、破坏语义。

session A在T1就说了,把d=5的行锁住,不准别的事务进行读写,此时被破坏。

因为如果我们这时插入d=5的数据,这条新的数据不在锁的保护范围之内。

2、数据一致性问题

锁的设计是为了保证数据的一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。

即使给所有行加上了锁,也避免不了幻读,这是因为给行加锁的时候,这条记录还不存在,没法加锁 。

也就是说即使把所有的记录都上锁了,还是阻止不了新插入的记录

如何解决幻读

产生的幻读的原因是:行锁只能锁住行

为了解决幻读问题,InnoDB引入新的锁:间隙锁(Gap Lock)

间隙锁,锁的就是两个值之间的空隙,比如在表t,初始化插入了6个记录,就产生了7个间隙:
在这里插入图片描述

执行:

select * from t where d = 5 for update

6个记录加上了行锁,同时加上了7个间隙锁。

间隙锁与行锁有点不一样

行锁可以分为读锁与写锁
在这里插入图片描述

与行锁有冲突关系的是另外一个行锁。

间隙锁不一样,间隙锁之间不存在冲突关系。

与间隙锁存在冲突关系的,是"向间隙中插入一个记录"这个操作。

举例:
在这里插入图片描述
由于表t中并没有c=7这个记录,所以session A加的是间隙锁(5,10)。而session B也是在这个间隙加的间隙锁,它们的目标都是保护这个间隙,不允许插入值,所以两者不冲突。

next-key lock

间隙锁与行锁合称next-key lock,每个lock都是前开后闭区间。间隙锁是开区间。

如上面我们插入数据,使用:

select * from t for update

形成了7个next-key lock,分别是:

(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]

supremum是一个不存在的最大值。

next-key lock 的引入解决了幻读问题,但是也带来了新的问题。

如,现在有这样一个业务逻辑:

任意锁住一行,如果这一行不存在的话就插入,如果存在这一行就更新它的数据。

begin;
select * from t where id = N for update;
--如果行不存在
insert into t values(N,N,N);
--如果行存在
update t set d = N set id = N;

commit;

现在出现这个现象:这个逻辑一旦有并发,就会碰到死锁。
在这里插入图片描述

死锁的产生:两个间隙锁不冲突,相互等待行锁

执行流程:

1、session A执行select…for update语句,由于id=9这一行不存在,因此会加上间隙锁(5,10)

2、session B执行select…for update语句,同样会加上间隙锁(5,10)

3、session B插入(9,9,9),被session A的间隙锁锁住,进入等待

4、session A擦汗如·插入(9,9,9),被session B的间隙锁锁住。

InnoDB死锁检测发现了这对死锁关系,然后报错返回了。

所以说间隙锁的引入可能会导致相同的语句锁住更大的范围,从而影响并发度。

间隙锁是在可重复读隔离级别下才会生效的。所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。 但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。

猜你喜欢

转载自blog.csdn.net/qq_42604176/article/details/115427771