当同一个查询在不同的时间产生不同的行集时,事务中就会出现所谓的幻影问题。例如,如果SELECT执行了两次,但第二次返回的行不是第一次返回的,则该行是“幻影”行。
假设子表的id列上有一个索引,并且您希望读取并锁定表中标识符值大于100的所有行,以便稍后更新所选行中的某些列:
SELECT * FROM child WHERE id > 100 FOR UPDATE;
查询从id大于100的第一条记录开始扫描索引。让表包含id值为90和102的行。如果在扫描范围内的索引记录上设置的锁定没有锁定在间隙(在这种情况下,是90和102之间的间隙)中进行的插入,则另一个会话可以将id为101的新行插入到表中。如果在同一事务中执行相同的SELECT,那么在查询返回的结果集中会看到一个id为101(“幻影”)的新行。如果我们将一组行视为一个数据项,那么新的phantom子项将违反事务的隔离原则,即事务应该能够运行,以便它读取的数据在事务期间不会更改。
为了防止出现幻影,InnoDB使用了一种称为next-key锁定的算法,该算法将索引行锁定和间隙锁定相结合。InnoDB执行行级锁定的方式是,当它搜索或扫描表索引时,它会对遇到的索引记录设置共享或独占锁。因此,行级锁实际上是索引记录锁。此外,索引记录上的下一个键锁定也会影响索引记录之前的“间隙”。也就是说,下一个密钥锁是索引记录锁加上索引记录之前间隙上的间隙锁。如果一个会话对索引中的记录R具有共享或独占锁定,则另一个会话无法在索引顺序中R之前的间隙中插入新的索引记录。
当InnoDB扫描索引时,它还可以锁定索引中最后一条记录之后的间隙。在前面的例子中就是这样:为了防止在id大于100的表中插入任何内容,InnoDB设置的锁包括id值102后面的间隙上的锁。
您可以使用下一个密钥锁定在应用程序中实现唯一性检查:如果您在共享模式下读取数据,但没有看到要插入的行的重复项,那么您可以安全地插入行,并知道在读取期间在行的后续行上设置的下一个键锁定可以防止任何人同时插入行的重复。因此,下一个密钥锁定使您能够“锁定”表中不存在的东西。
间隙锁定可以禁用,如第14.7.1节“InnoDB锁定”所述。这可能会导致幻影问题,因为当间隙锁定被禁用时,其他会话可以将新行插入间隙中。