Back

MySQL锁实践-幻读

发现问题

最近写的一个定时任务出现了主键冲突,数据插入失败了。我记得我有在插入前查询是否存在,但是仍然出现了这种情况。

翻阅日志发现,查询回来的记录数量为 0,插入又失败了。

我想到了几种情况:

  1. 数据在其他事务里插入了,但还没提交,导致这个事务里查询不存在但插入失败,因为我们的事务级别是可重复读;

  2. 主从架构,查询的从库,数据不是最新。

思索过后,发现第二张情况不满足,因为我们的测试环境只有一个数据库,那就只有第一种情况了。

使用行锁

所以,我把业务代码调整了下:

之前

  1. 查询记录

  2. 不存在则插入记录

  3. 使用查回来或者新建的对象

现在

  1. 使用 insert on duplicate key update 插入或者更新数据(当前事务锁定记录)

  2. 查询记录

  3. 使用查回来的对象

成功解决问题。

我知道会存在这种情况,但是之前这样写都没有发生过这种情况,也就放松了警惕。

使用间隙锁

如果说,要求一定要先查询,判断不存在在插入,而且不能发生主键冲突,那又该如果处理呢?

那只能在查询时加上锁了,可是记录不存在,那锁的是表吗?我猜可能是间隙锁,刚好趁这个机会,测试一下间隙锁。

首先,我先建立这样一张表

create table table_name
(
    id    int auto_increment primary key,
    value int null
);

插入测试数据

insert into table_name
    (id, value)
values
    (1, 1),
    (2, 3)

然后将 idea 改成 手动事务 模式

idea手动事务

查询并锁定不存在的记录,for update 用于查询时锁定记录

select *
from table_name
where id = 2
for update

在另一个连接(也就是另一个事务)里插入 id = 2 的记录,结果报获取锁超时,证实了锁的存在,那是表锁还是间隙锁呢?

idea新连接
insert into table_name
    (id, value)
values
    (2, 2)
间隙锁超时插入失败

试下插入 id = 4 的记录,插入成功

insert into table_name
    (id, value)
values
    (4, 4)
不在间隙锁范围 插入成功

试下删除 id = 1 和 3 的记录,删除成功,证明了不是表锁。

delete from table_name
where id (1, 3)
成功删除不在间隙锁范围的数据

然后我们再提交查询锁定 id = 2 记录的事务,再次执行插入 id = 2 的记录,成功插入。

释放间隙锁 插入成功

证实 select …​ for update 可以在不存在记录的情况下,可以使用间隙锁,避免在事务未提交的情况下,其他事务插入了查询锁住的 id 值记录。

其实吧,这个就是 幻读,我也是写完这篇文章后,才发现的,所以 MySQL 可重复读不能解决幻读,但是可以通过一些手段保障避免幻读产生。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy