17.7.4 幻读行
在事务中,当同一个查询在不同的时间返回不同的行集时,就会出现所谓的幻读问题。例如,如果执行了两个SELECT
语句,但第二次返回的行集中有第一次没有返回的行,那么这行就是一个幻读行。
假设在child
表中有一个索引,索引是基于id列的,并且你想在事务中读取和锁定所有child
表中的行,其中id值大于100,以便后续更新某个列:
SELECT * FROM child WHERE id > 100 FOR UPDATE;
查询从索引的第一个记录开始扫描,直到找到id值大于100的记录。假设表中包含id值为90和102的行。如果锁定索引记录在扫描范围内不锁定插入操作(在这个例子中是间隙之间),那么另一个会话可以将child
表中的新行插入到间隙之间,例如id值为101。如果你在同一个事务中执行相同的SELECT
语句,你将看到结果集中包含了id值为101的新行(一个幻读)。如果我们认为一组行是一个数据项,那么这个新的phantom child将违反事务的隔离原则,即事务应该能够运行,以便在事务中读取的数据不发生变化。
为了防止幻读,InnoDB
使用了一个算法称为临键锁,它结合了索引行锁和间隙锁。InnoDB
在搜索或扫描表索引时,将共享或独占锁设置在遇到的索引记录上。因此,这些行级锁实际上是索引记录锁。在此外,一次对索引记录的临键锁也影响了该索引记录之前的“间隙”。换言之,临键锁是一个索引记录锁加一个间隙锁,该间隙锁是指在索引记录前面的间隙。如果某个会话对索引记录R
设置了共享或独占锁,那么另一个会话不能在该索引顺序中插入新的索引记录在R
之前的间隙中。
当InnoDB
扫描索引时,也可以锁定该索引最后一个记录后的间隙。例如,在前面的示例中:为了防止将id
值设置为101以上的插入,InnoDB
设置的锁包括了在id
值102后面的间隙锁。
您可以使用临键锁来实现应用程序中的唯一性检查:如果您以共享模式读取数据并且没有看到要插入的行的副本,那么您可以安全地插入该行,并且知道在读取时设置的临键锁在您的行的后继记录上阻止了其他会话同时插入该行的副本。因此,临键锁使您能够“锁定”表中的某个东西不存在。
间隙锁定可以根据第17.7.1节,“InnoDB 锁定”进行禁用。这样可能会导致幻读问题,因为其他会话可以在禁用间隙锁定时将新行插入到空隙中。