MySQL解决幻读——MVCC与间隙锁

时间:2024-11-21 08:28:34

当前读 与 快照

当前读:

select...lock in share mode (共享读锁)
select...for update
update , delete , insert
  • 1
  • 2
  • 3

当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。

例如,假设要update一条记录,但是另一个事务已经delete这条数据并且commit了,如果不加锁就会产生冲突。所以update的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。

快照读:
简单的select操作(不包括 select … lock in share mode, select … for update)。

Read Committed隔离级别:每次select都生成一个快照读。

Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。

在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。

如何解决幻读

快照读的幻读是用MVCC解决的,当前的读的幻读是用间隙锁解决的。

innodb的默认事务隔离级别是rr(可重复读)。它的实现技术是mvcc该技术不仅可以保证innodb的可重复读,而且可以防止幻读。(这也就是是此前以rr隔离级别实践时,不仅可以防止可重复读,也防止了幻读)但是它防止的是快照读,也就是读取的数据虽然是一致的,但是数据是历史数据。

这个帖子里面就有一个实例:MySQL的InnoDB的幻读问题
一些文章写到InnoDB的可重复读避免了“幻读”(phantom read),这个说法并不准确。

那InnoDB指出的可以避免幻读是怎么回事呢?

以下翻译自MySQL官网文档(/pub6/mysql/doc/refman/5.5/en/),翻译水平一般,请见谅。

当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读。也就是说的间隙锁。

InnoDB提供了next-key locks,但需要应用程序自己去加锁

举个例子:

SELECT * FROM child WHERE id> 100 FOR UPDATE;
  • 1

该查询从id大于100 的第一条记录开始扫描索引 。如果该表包含具有id在90和102值的行。如果在扫描范围内的索引记录上设置的锁没有锁定在间隙中插入的内容(在这种情况下,在介于90和102之间,则另一个事务可以在表中插入一个新行,其行号id为101。如果您要在同一个事务中执行相同的SELECT,您将在查询返回的结果集中看到一个id为101的新行(一个“幻影”),这就产生了幻读。这与事务的隔离原则是相反的:一个事务应该能够运行,以便它已经读的数据在事务过程中不改变。如果我们把一套行视为数据项,新的“幽灵”子记录可能会违反这一隔离原则。

为了防止“幻影”,InnoDB使用了一种名为next-key locking的算法,它将 索引行锁定 和 间隙锁定 结合在一起。InnoDB执行行级锁的方式是,当它搜索或扫描一个表索引时,它会在遇到的索引记录上设置共享或独占锁。因此,行级锁实际上是索引记录锁。此外,索引记录上的next-key lock 也会影响该索引记录之前的“间隙”。也就是说,next-key lock 是一个index-record lock(索引记录锁)加上一个在索引记录之前的间隙上的 gap lock (间隙锁)。如果一个会话在索引中的记录R上具有共享锁或独占锁,则另一个会话不能在R之前的间隙中插入新的索引记录。

当InnoDB扫描一个索引之时,它也锁定所有记录中最后一个记录之后的间隙。刚在前一个例子中发生:InnoDB设置的锁定防止任何插入到id可能大过100的表。所有插入id为101的数据是无法执行的

您可以使用 next-key lock在应用程序中实现唯一性检查:如果你读数据时加了共享锁(select … from lock in share mode; 生成表级共享锁,允许其它线程读取数据但不能修改数据。)和没有看到你要复制的行插入,那么您可以安全地插入行并且明白:在读取期间,对您的行的后续行设置的next-key锁将防止任何人同时为您的行插入副本。因此,next-key锁允许您“锁定”表中不存在的内容。

假设我们有一个表:city,结构如下:
在这里插入图片描述
在第一个查询窗口中开始一个事务:

事务A 事务B
BEGIN;
SELECT * FROM city WHERE id > 2
BEGIN;
INSERT INTO city VALUES (6, ‘成都’);
COMMIT;
SELECT * FROM city WHERE id > 2
SELECT * FROM city WHERE id > 2 LOCK IN SHARE MODE

问,事务A的三个select分别是什么结果:

第一个:南京,广州,杭州
第二个:南京,广州,杭州
第三个:南京,广州,杭州,成都

我们可以看出,如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。如果需要实时显示数据,还是需要通过手动加锁来实现。这个时候会使用next-key技术来实现。

本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。InnoDB提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据: IN SHARE MODE;

我们再开启一个事务,执行:

BEGIN;
INSERT INTO city VALUES (7, '济南');
COMMIT;
  • 1
  • 2
  • 3

会怎样?
结果是阻塞。因为加了间隙锁

我们再开启一个事务,执行:

BEGIN;
UPDATE city SET name = '济南' WHERE ID = 1
UPDATE city SET name = '济南' WHERE ID = 2
COMMIT;
  • 1
  • 2
  • 3
  • 4

会怎样?
修改成功,没问题,间隙锁没加到这。

如果我写

BEGIN;
UPDATE city SET name = '济南' WHERE ID = 1
  • 1
  • 2

肯定会阻塞,因为间隙锁的原因。

结论:MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。

在mysql中,提供了两种事务隔离技术,第一个是mvcc,第二个是next-key技术。这个在使用不同的语句的时候可以动态选择。不加lock inshare mode之类的快照读就使用mvcc。否则 当前读使用next-key。mvcc的优势是不加锁,并发性高。缺点是不是实时数据。next-key的优势是获取实时数据,但是需要加锁。

另外,重要:

在rr级别下,mvcc完全解决了重复读,但并不能真正的完全避免幻读,只是在部分场景下利用历史数据规避了幻读

对于快照读,mysql使用mvcc利用历史数据部分避免了幻读(在某些场景看上去规避了幻读)

要完全避免,需要手动加锁将快照读调整为当前读(mysql不会自动加锁),然后mysql使用next-key完全避免了幻读,比如rr下,锁1(0,2,3,4),另一个线程的insert 3即被阻塞,在rc下,另一个线程仍然可以大摇大摆的插入,如本线程再次查询比如count,则会不一致

建议去看官方文档。

参考文章:
innodb当前读 与 快照读
MySQL的InnoDB的幻读问题
相关的官方文档
MySQL的锁机制 - 记录锁、间隙锁、临键锁
【MySQL】当前读、快照读、MVCC