Skip to the content.

..

Phantom Rows

参考:15.7.4 Phantom Rows

同一个事务中, 在不同的时间点执行相同的查询语句, 如果得到不同的结果集, 这种现象被称为幻读(phantom problem)。示例: 同一个 SELECT语句执行两次, 但第二次返回的结果比第一次查询多出了1行, 那么这1行就是 “幻影行(Phantom Row)”。

假设 child 表的 id 列有索引, 查询所有id值大于100 的行并进行锁定, 以便稍后进行更新:

SELECT * FROM child WHERE id > 100 FOR UPDATE;

这个查询从第一条id值大于100的记录开始扫描索引。 如果表中有两行数据的id值为90102, 在扫描范围内, 假如没有锁住(90102 之间的)间隙的话, 其他会话就可能在表中插入一个id值为101的新行。 在同一事务中再次执行相同的 SELECT 语句, 则查询返回的结果中会看到一个id为101的新行, 这就是幻影行。 如果将这种行视为数据项, 那么这条新的幻影数据将违反事务隔离原则: 已读取的数据在事务执行过程中不能被修改。

为了防止产生幻读, InnoDB 使用了一种叫做 “临键锁(next-key locking)” 的算法, 该算法组合使用了行锁(index-row locking)和间隙锁(gap locking)。 InnoDB 行级锁的执行方式, 是搜索或扫描索引时, 会在遇到的索引记录上设置共享锁(shared lock)或排他锁(exclusive lock)。 因此, 行级锁本质上是索引记录锁(index-record lock)。 此外, 索引记录上的临键锁还会影响该索引记录前面的”间隙”。 即, 临键锁, 是索引记录锁, 加上索引记录之前的间隙锁。如果一个会话在索引记录 R 上设置了临键锁(共享锁或排他锁), 按照索引的排序顺序, 其他会话不能在紧邻 R 之前的间隙中插入新的索引记录。

InnoDB扫描索引时, 也可能会锁定索引中最后一条记录后面的间隙。 前面的示例中我们演示了这种情况: 为了防止在表中插入 id 大于100的记录, InnoDB 设置的锁包括了 id 在 102 之后的间隙锁。

我们也可以用临键锁来实现唯一性检查: 以共享模式读取数据时, 如果没有看到要插入的行存在重复项, 则可以安全地插入行, 因为在读取时设置的临键锁, 可以防止其他会话在后面插入重复项。 事实上, 临键锁可以“锁定”表中并不存在的内容。

禁用间隙锁的方式请参考 15.7.1 InnoDB中的锁。 但可能会导致幻读问题, 因为禁用间隙锁之后, 其他会话有可能在间隙中插入新行。