Phantom Rows
同一个事务中, 在不同的时间点执行相同的查询语句, 如果得到不同的结果集, 这种现象被称为幻读(phantom problem)。示例: 同一个
SELECT
语句执行两次, 但第二次返回的结果比第一次查询多出了1行, 那么这1行就是 “幻影行(Phantom Row)”。
假设 child
表的 id
列有索引, 查询所有id值大于100
的行并进行锁定, 以便稍后进行更新:
SELECT * FROM child WHERE id > 100 FOR UPDATE;
这个查询从第一条id值大于100
的记录开始扫描索引。 如果表中有两行数据的id值为90
和102
, 在扫描范围内, 假如没有锁住(90
到 102
之间的)间隙的话, 其他会话就可能在表中插入一个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中的锁。 但可能会导致幻读问题, 因为禁用间隙锁之后, 其他会话有可能在间隙中插入新行。